{"id":22736948,"url":"https://github.com/asivery/qmldiff","last_synced_at":"2025-04-14T04:33:05.494Z","repository":{"id":266134772,"uuid":"894794956","full_name":"asivery/qmldiff","owner":"asivery","description":"A program for mass-editing QML trees","archived":false,"fork":false,"pushed_at":"2025-03-22T20:39:27.000Z","size":129,"stargazers_count":5,"open_issues_count":1,"forks_count":3,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-22T21:32:54.925Z","etag":null,"topics":["qml","qt"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/asivery.png","metadata":{"files":{"readme":"README.MD","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-27T02:15:58.000Z","updated_at":"2025-03-22T20:39:30.000Z","dependencies_parsed_at":"2024-12-02T19:40:01.351Z","dependency_job_id":"79af60da-dcaf-484f-9091-792ed22c285d","html_url":"https://github.com/asivery/qmldiff","commit_stats":null,"previous_names":["asivery/qmldiff"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asivery%2Fqmldiff","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asivery%2Fqmldiff/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asivery%2Fqmldiff/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asivery%2Fqmldiff/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/asivery","download_url":"https://codeload.github.com/asivery/qmldiff/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248821929,"owners_count":21166981,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["qml","qt"],"created_at":"2024-12-10T22:08:23.065Z","updated_at":"2025-04-14T04:33:05.486Z","avatar_url":"https://github.com/asivery.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# QMLDiff - a program for editing QML trees.\n\n## The diff language\n\n### Statements\n\n\u003e [!NOTE]\n\u003e Required parameters are marked as `\u003crequired\u003e`, and optional ones as `[optional]`\n\nQMLDiff defines its own language for modifying QML trees. The language's syntax is partially inspired by BASIC, and the JS DOM's querySelector syntax.\n\nIn the global scope, you are required to define which file\nis going to be alrered. To do that, use the `AFFECT` keyword.\n\nExample:\n```\nAFFECT /qml/TestQML.qml\n    ; Diff statements go here.\nEND AFFECT\n```\n\nAlternatively, you can add data to a `SLOT`. `SLOT`s' contents will be written to the final re-emitted QML in place of `INSERT SLOT \u003cslot\u003e`, or QML `~{slot}~` statements.\n\nExample:\n```\nSLOT slot\n    INSERT {\n        // QML Tree goes here.\n    }\nEND SLOT\n```\nYou can only use `INSERT` directives within `SLOT` declarations.\n\nWithin `ALTER` statements, you can use the following DIFF directives:\n\n#### `TRAVERSE \u003ctree\u003e`\n\nThe traverse statement changes the current root of the file being processed.\n\nAssume the following QML file:\n```\nimport test.Test 1.0\n\nRectangle {\n    Item {\n        color: \"black\"\n    }\n    Item {\n        color: \"red\"\n    }\n    Item {\n        color: \"green\"\n    }\n}\n```\n\nTo place your cursor within the red `Item`, use the following statment:\n```\nTRAVERSE Rectangle \u003e Item[.color=\"\\\"red\\\"\"]\n```\n\n`TRAVERSE` blocks can included in one another, to modify objects deeper in the tree structure of\nthe current root. Because of that, every traverse block needs to be terminated with `END TRAVERSE`\n\n#### `LOAD \u003cfile_path\u003e`\n\nThe load statements loads the file with the path given as a QMLDiff file.\n\n#### `ASSERT \u003ctree\u003e`\n\nThe ASSERT statement disambiguates a TRAVERSE statement by selecting only such roots, that\ncontain a node matching the given filter. It does not change the root, or move the cursor.\n\nAssume the following QML file:\n```\nimport test.Test 1.0\n\nRectangle {\n    Item {\n        color: \"black\"\n    }\n    Item {\n        color: \"red\"\n        Object {\n            OtherObject {\n                value: a\n            }\n        }\n    }\n    Item {\n        color: \"red\"\n        Object {\n            OtherObject {\n                value: b\n            }\n        }\n    }\n}\n```\n\nAfter issuing the instruction `TRAVERSE Rectangle \u003e Item[.color=\"\\\"red\\\"\"]`, it would be\nambiguous in which item we are currently located.\n\nTo narrow down the root to the first Item object, after the `TRAVERSE` statement, you need to issue\nthe following statement: `ASSERT Object \u003e OtherObject[.value=a]`\n\n#### `LOCATE \u003cBEFORE/AFTER\u003e \u003ctree/ALL\u003e`\n\nThe `LOCATE` statement moves the cursor within the current QML tree object to `BEFORE`/`AFTER` the first element matching the `tree`, or all elements.\n\nAssume the following QML file:\n```\nimport test.Test 1.0\n\nRectangle {\n    Item {\n        color: \"black\"\n    }\n    Item {\n        color: \"red\"\n        Object {\n            OtherObject {\n                value: a\n            }\n        }\n    }\n    Item {\n        color: \"red\"\n        Object {\n            OtherObject {\n                value: b\n            }\n        }\n    }\n}\n```\n\nTo move the cursor inbetween the two red items, the following statement would need to be issued (assuming the current root is the Rectangle object):\n```\nLOCATE AFTER Item \u003e Object \u003e OtherObject[.value=a]\n```\n\n#### `INSERT SLOT \u003cslot\u003e`\n\nInserts a named slot at the current cursor position.\n\n#### `INSERT { QML }`\n\nInserts the QML code at the current cursor position. It's possible to declare slots within the QML Code by using the `~{slotName}~` syntax:\n\nAssume the following QML file:\n```\nimport test.Test 1.0\n\nRectangle {\n    Item {\n        color: \"black\"\n    }\n    Item {\n        color: \"red\"\n        Object {\n            OtherObject {\n                value: a\n            }\n        }\n    }\n    Item {\n        color: \"red\"\n        Object {\n            OtherObject {\n                value: b\n            }\n        }\n    }\n}\n```\n\nAfter executing the following code:\n\n```\nTRAVERSE Rectangle\nLOCATE AFTER Item \u003e Object \u003e OtherObject[.value=a]\nINSERT {\n    TestObject {\n        function processObject(){\n            for(let i = 0; i\u003c10; i++){\n                ~{slotLoop}~\n            }\n        }\n    }\n}\nEND TRAVERSE\n\n;-----------In global context-----------;\nSLOT slotLoop\n    INSERT {\n        console.log(i);\n    }\nEND SLOT\n```\n\nThe QML code would be changed to:\n\n```\nimport test.Test 1.0\n\nRectangle {\n    Item {\n        color: \"black\"\n    }\n    Item {\n        color: \"red\"\n        Object {\n            OtherObject {\n                value: a\n            }\n        }\n    }\n    TestObject {\n        function processObject(){\n            for(let i = 0; i\u003c10; i++){\n                console.log(i);\n            }\n        }\n    }\n    Item {\n        color: \"red\"\n        Object {\n            OtherObject {\n                value: b\n            }\n        }\n    }\n}\n```\n\n\n#### `REMOVE \u003cnode\u003e`\n\nDeletes all children matching the `\u003cnode\u003e` selector from the current root.\n\n#### `REPLACE \u003cnode\u003e WITH { QML }`\n\nThis statement can be treated as a combination of the `LOCATE`, `REMOVE` and `INSERT` statements.\nIt locates the first child matching the `\u003cnode\u003e` selector within the current root, deletes it, then inserts\nthe QML code provided at that spot.\n\n#### `REPLICATE \u003ctree\u003e`\n\nThe `REPLICATE` statement finds the node pointed to by `tree` in the current root, then clones it into a new fake-root that's outside of the currently edited file's tree. It then immediately `TRAVERSE`s that new root. This makes it possible to use any statements used within `TRAVERSE` blocks to freely edit the object.\n\nNeeds to be termiated with `END REPLICATE` - that exits the fake root and merges it back into the tree, at the position pointed to by the current root's cursor.\n\n#### `RENAME \u003cnode\u003e TO \u003cid\u003e`\n\nRenames the first child matching the `\u003cnode\u003e` selector to `\u003cid\u003e`. It can only be used for named objects declarations (and not objects!).\n\nUseful for when a function needs to be replaced. This statement makes it possible to simply rename the original function to something else, then insert a new one\nnamed after the original, which invokes its predecessor.\n\nUpdates the cursor to after the renamed element.\n\n#### `IMPORT \u003cobject\u003e \u003cversion\u003e [alias]`\n\nThis statement can only be used within the direct scope of the `AFFECT` block (i.e. Not in a `SLOT` or `TRAVERSE` block).\nIt adds an import to the top of the QML file.\n\n#### `REBUILD \u003cproperty\u003e` / `REDEFINE \u003cproperty\u003e`\n\nThis statement is only valid for JS functions, object and non-object assignments, object and non-object properties and objects themselves. It rebuilds the token stream the value consists of.\nIt's a block statement, so you need to end the `REBUILD` block with `END REBUILD`.\nThe rebuild block essentially defines its own separate language, described below.\n\nWithin the `REBUILD` block, another cursor is created. The commands within it move the cursor around, assert whether or not the expected data is located at the cursor and edit the tokens the value is made from. There also exists a special \"variable\" called `LOCATED`, which sometimes can be used instead of providing the token stream. The contents of that variable are updated by some statements.\n\n\u003e [!NOTE]\n\u003e QML token streams can either be provided by enclosing them in curly braces: `{ qmlCodeGoesHere }` or, in case of non-valid QML blocks: `STREAM \u003cending_token\u003e qmlCodeGoesHere \u003cending_token\u003e`\n\u003e Example:\n\u003e `STREAM / if(a) { /`\n\nThe difference between `REBUILD` and `REDEFINE` is: `REDEFINE` lets you change the way the property is defined, as well as insert / remove additional objects, whereas `REBUILD` makes that impossible.\n\nExample - for this QML tree when rebuilding / redefining `a`:\n\n```qml\nObject {\n    a: ObjectA {\n        text: \"bbb\"\n    }\n}\n```\n\nThe stream would consist of the following following for `REBUILD`:\n\n```\nIdentifier(\"ObjectA\") Symbol('{') Identifier(\"text\")...\n```\n\nWhereas for `REDEFINE`:\n\n```\nIdentifier(\"a\") Symbol(':') Identifier(\"ObjectA\") Symbol('{') Identifier(\"text\")...\n```\n\n\n##### `INSERT ARGUMENT \u003cname\u003e AT \u003cposition\u003e`\n\nThis statement is only valid for functions - it adds an argument to the list of arguments in the function declaration. `position` is zero-indexed.\n\n##### `REMOVE ARGUMENT \u003cname\u003e AT \u003cposition\u003e`\n\nRemoves the argument called `name` at `position`. Only valid for functions.\n\n##### `RENAME ARGUMENT \u003cname\u003e AT \u003cposition\u003e TO \u003cnew_name\u003e`.\n\nRenames the argument called `name` located at `position` to `new_name`. Only valid for functions.\n\n##### `INSERT \u003cQML Code\u003e`\n\nInserts the provided QML code at the cursor position.\n\n##### `REMOVE \u003cQML Code\u003e` / `REMOVE LOCATED`\n\nChecks if the provided QML code is at the current cursor position, then removes it.\n\n##### `REMOVE UNTIL END` / `REMOVE UNTIL \u003cQML Code\u003e`\n\nRemoves all tokens until end of stream / until the provided QML Code is found.\n\n##### `LOCATE [BEFORE / AFTER] ALL`\n\nSets the cursor to either the beginning, or the end of the stream and clears the `LOCATED` variable.\n\n##### `LOCATE [BEFORE / AFTER] \u003cQML Code\u003e`\n\nSets the cursor to either before or after the provided QML code. Sets the `LOCATED` variable to that code.\n\n##### `REPLACE [LOCATED / \u003cQML Code\u003e] UNTIL \u003cQML Code\u003e WITH \u003cQML Code\u003e`\n\nStarting at the current cursor position, tries to find all instances of either the contents of the `LOCATED` variable or the provided QML stream, and replaces it with the provided value until the QML code in the `UNTIL` clause is encountered.\n\n##### `REPLACE [LOCATED / \u003cQML Code\u003e] WITH \u003cQML Code\u003e`\n\nSee above, but always replaces until the end of stream is encountered.\n\n\n### Selectors\n\n#### Tree Selector\n\nThe tree selector consists of multiple node selectors delimeted with the '\u003e' character\n\n#### Node selector\n\nNode selectors can either be simply the name of a given property of a QML object, or a complex selector that checks multiple aspects of a given object.\n\nIn the case of the latter, the selector follows the format:\n\n```\nObjectName[property1][property2]...\n```\n\nProperties can verify:\n\n- Object name within the parent (`:name`)\n- Existence of a given property (`!prop`)\n- Equality of a given property (`.prop=value`) *\n- Whether or not a given property contains some string (`.prop~value`) *\n- The id of a given object (`#root`) (really just syntax sugar for `.id=root`)\n\n\\* - The value is checked as-is, but it can be provided as a string. For example, the selectors `Object[.value=test]` or `Object[.value=\"test\"]` won't match the QML object `Object { value: \"test\" }`. Instead, you need to use `Object[.value=\"\\\"test\\\"\"]`.\n\n\nThe `[]` characters are ignored within selectors. `Object[.name=test]` is equal to `Object.name=test`.\n\n\n### Hashing\n\nQMLDiff's diff files can be hashed to not refer to objects, properties or values by their actual names.\nInstead, hashes can be used.\n\nTake the following diff file:\n\n```\nAFFECT /test.qml\n    TRAVERSE RootObject\n        LOCATE BEFORE ALL\n        INSERT { property bool myValue: false }\n\n        REPLACE visible WITH {\n            visible: !global.visible \u0026\u0026 myValue\n        }\n    END TRAVERSE\nEND AFFECT\n```\n\nTo QMLDiff, it is identical to:\n\n```\nAFFECT [[254452526029728816]]\n    TRAVERSE [[8398551154981323716]]\n        LOCATE BEFORE ALL\n        INSERT { property bool myValue: false }\n\n        REPLACE [[233748328658231]] WITH {\n            ~\u0026233748328658231\u0026~: !~\u00267082699062074\u0026~.~\u0026233748328658231\u0026~ \u0026\u0026 myValue\n        }\n    END TRAVERSE\nEND AFFECT\n```\n\nIn order to retrieve the original names, QMLDiff uses `hashtab` files.\nIn the case of the aforementioned example, the hashtab consists of the following entries:\n\n```\n8398551154981323716 = \"RootObject\"\n254452526029728816 = \"/test.qml\"\n233748328658231 = \"visible\"\n7082699062074 = \"global\"\n```\n\n### Templates\n\nQMLDiff supports diff templating. Templates can be defined, then used in multiple places. They act like macros. Templates are completely separate from slots, and slots cannot be used with them in any way (of course templates can still be inserted into slots).\n\n#### Defining a template\n\nTo define a template, use a `TEMPLATE` directive:\n\n```\nTEMPLATE SomeTemplate {\n    ObjectToBeTemplated {\n        someValue: \"constant\"\n        alwaysTrue: true\n        name: ~{name}~\n\n        ChildObject {\n            childValue: ~{child}~\n        }\n    }\n}\n```\n\nTemplates work by defining an internal slot scope. That means that anything that would be a slot in normal diff code, would become a property in the template.\n\n#### Inserting a template\n\nInserting a template can be done using an `INSERT TEMPLATE` directive:\n\n```\nINSERT TEMPLATE SomeTemplate {\n    child: 'SomeChildValue'\n    name: 'Some test object that uses templates'\n}\n```\n\n#### More complex examples\n\nTemplates can also pass whole objects, or objects from slots \n\nThis file:\n\n```\nObject {\n\tsomeValue: 10\n\tSomething {\n\t\ta: 10\n\t}\n\tSomething {\n\t\tb: 10\n\t}\n}\n```\n\nCan get translated into this file:\n\n```\nObject {\n    someValue: 10\n    Something {\n        a: 10\n        test: 100000\n        ObjectToBeTemplated {\n            someValue: \"constant\"\n            alwaysTrue: true\n            name: \"Test Object\"\n            ChildObject {\n                ObjectChildA {\n                    childa: true\n                }\n\n                ObjectChildB {\n                    childb: true\n                }\n            }\n        }\n    }\n\n    Something {\n        b: 10\n    }\n}\n```\n\nUsing this template:\n\n```\nTEMPLATE SomeTemplate {\n    ObjectToBeTemplated {\n        someValue: \"constant\"\n        alwaysTrue: true\n        name: ~{name}~\n\n        ChildObject {\n            ~{children}~\n        }\n    }\n}\n\nAFFECT /main.qml\n    TRAVERSE Object \u003e Something[.a=10] \n        LOCATE AFTER ALL INSERT {\n            test: 100000\n        }\n        INSERT TEMPLATE SomeTemplate {\n            name: \"Test Object\"\n            children: ObjectChildA {\n                childa: true\n            }\n            children: ObjectChildB {\n                childb: true\n            }\n        }\n    END TRAVERSE\nEND AFFECT\n```\n\n## Using QMLDiff as a command-line tool:\n\nQMLDiff can be used as a command-line tool.\n\nRight now the following subcommands are supported:\n\n- create-hashtab `\u003cQML root\u003e [output hashtab path]`\n    * Creates a hashtab file from all the files within `QML root` recursively.\n- hash-diffs `\u003chashtab\u003e \u003cdiff 1\u003e [diff 2]... [-r]`\n    * Turns all the diffs provided into their hashed versions (using the provided hashtab). This operation changes the diffs IN PLACE!\n    * `-r` flag reverts this operation.\n- apply-diffs `\u003chashtab\u003e \u003cQML root\u003e \u003cQML destination\u003e [...diffs] [-f] [-c]`\n    * Applies all the provided diffs to the QML files within QML root, then writes the results to QML destination.\n    * `-f` flattens the output file tree into the root directory\n    * `-c` deletes the QML destination directory before applying the diffs.\n\n## Using QMLDiff as a library:\n\nQMLDiff can be used as a C library. It exports the following functions:\n\n- `int qmldiff_build_change_files(const char *rootDir)`\n    * Loads all the diff files from rootDir\n    * Returns the amount of files read\n- `char *qmldiff_process_file(const char *fileName, char *contents, size_t contentsLength)`\n    * Processes a single QML file using diffs loaded via `qmldiff_build_change_files`\n    * Returns NULL in case of an error, or when no changes were performed. Newly allocated string containing the re-emitted QML otherwise\n- `char qmldiff_is_modified(const char *fileName)`\n    * Checks if any diff affects the file `fileName`\n    * Returns true if they do, false otherwise\n- `void qmldiff_start_saving_thread()`\n    * Starts the hashtab-exporting thread *\n    * Should be called as part of the initialization sequence of your program.\n- `void qmldiff_load_rules(const char *rules)`\n    * Sets the global hashtab-creation rules to the argument given\n    * `rules` are meant to be passed as a raw string containing the hashtab rules. Not a file path!\n\n\\* - In order to create a hashtab when QMLDiff is utilized as a library, please set the `QMLDIFF_HASHTAB_CREATE` environment variable to the desired path where the hashtab file is to be kept. This will essentially disable all the diff-applying functionality of QMLDiff. It will be saving the current state of the global hashtab into the desired file every minute, until terminated.\n\n\n## TODOs:\n\n- [ ] Better error handling - currently both syntax and processing errors are ambiguous\n- [ ] Better documentation\n- [ ] Better emitters - the current ones make the output QML a bit unreadable\n- [ ] Better method of exporting hashtab when running as a library\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasivery%2Fqmldiff","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fasivery%2Fqmldiff","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasivery%2Fqmldiff/lists"}