From 24b433211d8142602b6dfcc59afe1bca9cab1ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Desmedt?= <34781168+DesignThinkerer@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:17:34 +0200 Subject: [PATCH] feat: add JSON output support to makepatches operator (#9940) * feat: enhance makepatches function to support multiple suffixes and JSON output format * feat: add release notes for makepatches JSON output enhancement * doc: update makepatches operator documentation to document new json output * doc: add JSON output examples to makepatches and applypatches documentation * chore: convert space indentations to tabs * test: add tests for makepatches operator with JSON output support * chore: change space to tab indentation * test: add DiffMergePatch4 integration test for testing makepatches json output * fix: remove superfluous heading character * refactor the parsing of the makepatches suffixes Co-authored-by: Saq Imtiaz * refactor: improve makepatches function for better readability and performance * chore: apply style guide by collapsing consecutive variable assignments * chore: remove extraneous tabs --------- Co-authored-by: Saq Imtiaz --- core/modules/filters/strings.js | 39 ++++++++++++------- .../tests/data/filters/DiffMergePatch4.tid | 22 +++++++++++ .../test/tiddlers/tests/test-json-filters.js | 15 +++++++ ...s and applypatches Operator (Examples).tid | 10 ++++- .../tiddlers/filters/makepatches Operator.tid | 17 +++++--- .../5.5.0/#9940-makepatches-json-output.tid | 12 ++++++ 6 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 editions/test/tiddlers/tests/data/filters/DiffMergePatch4.tid create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9940-makepatches-json-output.tid diff --git a/core/modules/filters/strings.js b/core/modules/filters/strings.js index 854c43b8c6..6a93715964 100644 --- a/core/modules/filters/strings.js +++ b/core/modules/filters/strings.js @@ -91,22 +91,35 @@ function diffLineWordMode(text1,text2,mode) { return diffs; } -exports.makepatches = function(source,operator,options) { - var suffix = operator.suffix || "", - result = []; - - source(function(tiddler,title) { - let diffs, patches; - if(suffix === "lines" || suffix === "words") { - diffs = diffLineWordMode(title,operator.operand,suffix); - patches = dmp.patchMake(title,diffs); +exports.makepatches = function(source, operator, options) { + const suffixes = operator.suffixes || [], + [modeArg = [], formatArg = []] = suffixes, + modeSuffix = modeArg[0] || operator.suffix || "", + mode = ["lines", "words"].includes(modeSuffix) ? modeSuffix : "", + isJson = formatArg[0] === "json", + results = []; + + source((tiddler, title) => { + if (isJson) { + const diffs = (mode === "lines" || mode === "words") + ? diffLineWordMode(title, operator.operand, mode) + : dmp.diffMain(title, operator.operand); + + const jsonOutput = diffs.map(([typeCode, text]) => ({ + type: typeCode === 1 ? "insert" : (typeCode === -1 ? "delete" : "equal"), + text + })); + results.push(JSON.stringify(jsonOutput)); } else { - patches = dmp.patchMake(title,operator.operand); + const patches = (mode === "lines" || mode === "words") + ? dmp.patchMake(title, diffLineWordMode(title, operator.operand, mode)) + : dmp.patchMake(title, operator.operand); + + results.push(dmp.patchToText(patches)); } - Array.prototype.push.apply(result,[dmp.patchToText(patches)]); }); - return result; + return results; }; exports.applypatches = makeStringBinaryOperator( @@ -235,4 +248,4 @@ exports.charcode = function(source,operator,options) { } }); return [chars.join("")]; -}; +}; \ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/filters/DiffMergePatch4.tid b/editions/test/tiddlers/tests/data/filters/DiffMergePatch4.tid new file mode 100644 index 0000000000..6c56a379fb --- /dev/null +++ b/editions/test/tiddlers/tests/data/filters/DiffMergePatch4.tid @@ -0,0 +1,22 @@ +title: Filters/DiffMergePatch4 +description: Tests for diff-merge-patch derived operators +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +\define text1() +The quick brown fox +\end + +\define text2() +The fast brown fox +\end + +<$text text={{{ [makepatches::json] }}}/> + ++ +title: ExpectedResult + +[{"type":"equal","text":"The "},{"type":"delete","text":"quick"},{"type":"insert","text":"fast"},{"type":"equal","text":" brown fox"}] \ No newline at end of file diff --git a/editions/test/tiddlers/tests/test-json-filters.js b/editions/test/tiddlers/tests/test-json-filters.js index 1232e5cf88..c686089485 100644 --- a/editions/test/tiddlers/tests/test-json-filters.js +++ b/editions/test/tiddlers/tests/test-json-filters.js @@ -173,5 +173,20 @@ describe("json filter tests", function() { expect(wiki.filterTiddlers("[{First}format:json[ ]]")).toEqual(["{\n \"a\": \"one\",\n \"b\": \"\",\n \"c\": 1.618,\n \"d\": {\n \"e\": \"four\",\n \"f\": [\n \"five\",\n \"six\",\n true,\n false,\n null\n ]\n }\n}"]); }); + it("should support the makepatches operator with json output", function() { + expect(wiki.filterTiddlers("[[The quick brown fox]makepatches::json[The fast brown fox]]")).toEqual([ + '[{"type":"equal","text":"The "},{"type":"delete","text":"quick"},{"type":"insert","text":"fast"},{"type":"equal","text":" brown fox"}]' + ]); + + expect(wiki.filterTiddlers("[[The quick brown fox]makepatches:words:json[The fast brown fox]]")).toEqual([ + '[{"type":"equal","text":"The "},{"type":"delete","text":"quick "},{"type":"insert","text":"fast "},{"type":"equal","text":"brown fox"}]' + ]); + + // Safely ignores missing/invalid second suffix and returns a standard patch string instead of JSON + expect(wiki.filterTiddlers("[[The quick brown fox]makepatches:words[The fast brown fox]]")[0]).not.toContain( + '"type":"equal"' + ); + }); + }); diff --git a/editions/tw5.com/tiddlers/filters/examples/makepatches and applypatches Operator (Examples).tid b/editions/tw5.com/tiddlers/filters/examples/makepatches and applypatches Operator (Examples).tid index a9dd38d6ee..f1ae2b8611 100644 --- a/editions/tw5.com/tiddlers/filters/examples/makepatches and applypatches Operator (Examples).tid +++ b/editions/tw5.com/tiddlers/filters/examples/makepatches and applypatches Operator (Examples).tid @@ -1,5 +1,5 @@ created: 20230304160331362 -modified: 20230304160332927 +modified: 20260724082228670 tags: [[makepatches Operator]] [[applypatches Operator]] [[Operator Examples]] title: makepatches and applypatches Operator (Examples) type: text/vnd.tiddlywiki @@ -40,4 +40,12 @@ The `lines` mode doesn't work as well in this application: It is better suited as a very fast algorithm to detect line-wise incremental changes to texts and store only the changes instead of multiple versions of the whole texts. +The `json` suffix outputs a structured JSON array representing the differences instead of a patch string. To use the JSON output with the default character mode, skip the first suffix with a double colon (`::`). + +<<.operator-example 8 "[{Hamlet##Shakespeare-old}makepatches::json{Hamlet##Shakespeare-new}]">> + +The `json` suffix can also be combined with the `words` or `lines` modes: + +<<.operator-example 9 "[{Hamlet##Shakespeare-old}makepatches:words:json{Hamlet##Shakespeare-new}]">> + \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/filters/makepatches Operator.tid b/editions/tw5.com/tiddlers/filters/makepatches Operator.tid index c59284e22d..37a223d766 100644 --- a/editions/tw5.com/tiddlers/filters/makepatches Operator.tid +++ b/editions/tw5.com/tiddlers/filters/makepatches Operator.tid @@ -1,23 +1,28 @@ caption: makepatches created: 20230304122354967 -modified: 20230304122400128 -op-purpose: returns a set of patches that transform the input to a given string +modified: 20260724081502905 op-input: a [[selection of titles|Title Selection]] +op-output: a set of patch instructions per input title to be used by the [[applypatches Operator]] to transform the input title(s) into the string <<.place S>>, or a structured JSON array representing the differences if the `json` suffix is used op-parameter: a string of characters op-parameter-name: S -op-output: a set of patch instructions per input title to be used by the [[applypatches Operator]] to transform the input title(s) into the string <<.place S>> -op-suffix: `lines` to operate in line mode, `words` to operate in word mode. If omitted (default), the algorithm operates in character mode. See notes below. -op-suffix-name: T +op-purpose: returns a set of patches that transform the input to a given string +op-suffix: optional suffixes specifying the diff mode and output format +op-suffix-name: T:F tags: [[Filter Operators]] [[String Operators]] title: makepatches Operator type: text/vnd.tiddlywiki <<.from-version "5.2.6">> +The <<.op makepatches>> operator uses up to two suffixes: + +* <<.place T>> (diff mode): `lines` to operate in line mode, `words` to operate in word mode. If omitted (default), the algorithm operates in character mode. +* <<.place F>> (output format): <<.from-version "5.5.0">> `json` to output a structured JSON array of differences instead of a standard patch string. To use the JSON output with the default character mode, skip the first suffix with a double colon (e.g., `makepatches::json`). + The difference algorithm operates in character mode by default. This produces the most detailed diff possible. In `words` mode, each word in the input text is transformed into a meta-character, upon which the algorithm then operates. In the default character mode, the filter would find two patches between "ActionWidget" and "Action-Widgets" (the hyphen and the plural s), while in `words` mode, the whole word is found to be changed. In `lines` mode, the meta-character is formed from the whole line, delimited by newline characters, and is found to be changed independent of the number of changes within the line. The different modes influence the result when the patches are applied to texts other than the original, as well as the runtime. <<.tip "The calculation in `words` mode is roughly 10 times faster than the default character mode, while `lines` mode can be more than 100 times faster than the default.">> -<<.operator-examples "makepatches and applypatches">> +<<.operator-examples "makepatches and applypatches">> \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9940-makepatches-json-output.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9940-makepatches-json-output.tid new file mode 100644 index 0000000000..3d9fb165f9 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9940-makepatches-json-output.tid @@ -0,0 +1,12 @@ +change-category: filters +change-type: enhancement +created: 20260724080519370 +description: The makepatches operator can now output structured JSON +github-contributors: DesignThinkerer +github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9940 +release: 5.5.0 +tags: $:/tags/ChangeNote +title: $:/changenotes/5.5.0/#9940 +type: text/vnd.tiddlywiki + +* The [[makepatches|makepatches Operator]] operator now supports a `:json` suffix (e.g., `makepatches::json`) that outputs a structured JSON array of the differences, enabling easier and robust parsing with WikiText \ No newline at end of file