diff --git a/packages/preview/palimset/0.1.0/LICENSE b/packages/preview/palimset/0.1.0/LICENSE new file mode 100644 index 0000000000..c37ec4b853 --- /dev/null +++ b/packages/preview/palimset/0.1.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Takeru Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/preview/palimset/0.1.0/README.md b/packages/preview/palimset/0.1.0/README.md new file mode 100644 index 0000000000..dff52afddb --- /dev/null +++ b/packages/preview/palimset/0.1.0/README.md @@ -0,0 +1,58 @@ +# palimset + +This is a library for displaying document differences in Typst. +“Palimset” is a coined word combining the terms ‘palimpsest’ and ‘set’. + +## Usage + +### diff-string + +Compares two strings and highlights the differences. + +```typst +#import "@preview/palimset:0.1.0": * + +#let a = "hello, workd. こんばんは" +#let b = "hello, world. こんにちは" + +#diff-string(a, b) +``` + +The output will look like this: + + + +### diff-content + +Compares two Typst contents and highlights the differences, preserving styles. + +```typst +#import "@preview/palimset:0.1.0": * + +#diff-content( + include "diff-a.typ", + include "diff-b.typ" +) +``` + + + +## Functions + +### `diff-string(a, b, format-plus, format-minus, split-regex)` + +- `a`, `b`: The two strings to compare. +- `format-plus`: A function to format added text. (Default: `x => text(x, fill: blue, weight: "bold")`) +- `format-minus`: A function to format removed text. (Default: `x => strike(text(x, fill: red, size: 0.75em))`) +- `split-regex`: A regular expression string to split the strings for comparison. (Default: `"[^A-Za-z0-9]"`) + +### `diff-content(a, b, format-plus, format-minus, split-regex)` + +- `a`, `b`: The two Typst contents to compare. +- `format-plus`: A function to format added text. (Default: `x => text(x, fill: blue, weight: "bold")`) +- `format-minus`: A function to format removed text. (Default: `x => strike(text(x, fill: red, size: 0.75em))`) +- `split-regex`: A regular expression string to split the contents for comparison. (Default: `"[^A-Za-z0-9]"`) + +## License + +This project is licensed under the MIT License. diff --git a/packages/preview/palimset/0.1.0/diff-content.typ b/packages/preview/palimset/0.1.0/diff-content.typ new file mode 100644 index 0000000000..e65c5b8a26 --- /dev/null +++ b/packages/preview/palimset/0.1.0/diff-content.typ @@ -0,0 +1,155 @@ +#import "diff-function.typ": * +#import "@preview/touying:0.6.1": utils + +#let is-text(it) = ( + it != none and (it.func() == text) +) + +#let sequence-text(it, arr) = {} + +#let styled-text(it, arr) = { + if utils.is-sequence(it) { + for val in it.children { + arr = styled-text(val, arr) + } + } else if utils.is-styled(it) { + arr = styled-text(it.child, arr) + } else if is-text(it) { + arr.push(it.text) + } + + return arr +} + +#let content-text(it) = { + let output_arr = () + for val in it.children { + if is-text(val) { + output_arr.push(val.text) + } else if utils.is-styled(val) { + output_arr = styled-text(val.child, output_arr) + } + } + return output_arr +} + +#let styled-output(it, arr, format-plus, format-minus) = { + if utils.is-sequence(it) { + let output = () + for val in it.children { + let tmp = styled-output(val, arr, format-plus, format-minus) + output.push(tmp.at(0)) + arr = tmp.at(1) + } + if output.len() == 0 { + return (none, arr) + } + return (output.sum(), arr) + } else if utils.is-styled(it) { + let output = () + let tmp = styled-output(it.child, arr, format-plus, format-minus) + arr = tmp.at(1) + return (utils.reconstruct-styled(it, tmp.at(0)), arr) + } else if is-text(it) { + let text_len = it.text.len() + let start = 0 + let output = () + for value in range(arr.at(0).len()) { + if arr.at(1).at(0) == 0 { + let tmp = arr.at(0).remove(0) + start += tmp.len() + if start < text_len { + let _ = arr.at(1).remove(0) + output.push(text(tmp)) + } else { + output.push(text(tmp.slice(0, tmp.len() + text_len - start))) + arr.at(0).insert(0, tmp.slice(tmp.len() + text_len - start, tmp.len())) + break + } + } else if arr.at(1).at(0) == 1 { + let tmp = arr.at(0).remove(0) + start += tmp.len() + if start < text_len { + let _ = arr.at(1).remove(0) + output.push(format-plus(tmp)) + } else { + output.push(format-plus(tmp.slice(0, tmp.len() + text_len - start))) + arr.at(0).insert(0, tmp.slice(tmp.len() + text_len - start, tmp.len())) + break + } + } else if arr.at(1).at(0) == -1 { + let _ = arr.at(1).remove(0) + output.push(format-minus(arr.at(0).remove(0))) + } + } + + return (output.sum(), arr) + } else { + return (it, arr) + } +} + +#let content-output( + it, + arr, + format-plus, + format-minus, +) = { + for val in it.children { + if is-text(val) { + let text_len = val.text.len() + let start = 0 + for value in range(arr.at(0).len()) { + if arr.at(1).at(0) == 0 { + let tmp = arr.at(0).remove(0) + start += tmp.len() + if start < text_len { + let _ = arr.at(1).remove(0) + text(tmp) + } else { + text(tmp.slice(0, tmp.len() + text_len - start)) + arr.at(0).insert(0, tmp.slice(tmp.len() + text_len - start, tmp.len())) + break + } + } else if arr.at(1).at(0) == 1 { + let tmp = arr.at(0).remove(0) + start += tmp.len() + if start < text_len { + let _ = arr.at(1).remove(0) + format-plus(tmp) + } else { + format-plus(tmp.slice(0, tmp.len() + text_len - start)) + arr.at(0).insert(0, tmp.slice(tmp.len() + text_len - start, tmp.len())) + break + } + } else if arr.at(1).at(0) == -1 { + let _ = arr.at(1).remove(0) + format-minus(arr.at(0).remove(0)) + } + } + } else if utils.is-styled(val) { + let tmp = styled-output(val.child, arr, format-plus, format-minus) + utils.reconstruct-styled(val, tmp.at(0)) + arr = tmp.at(1) + } else { + val + } + } + + let aaa = arr +} + +#let diff-content( + a, + b, + format-plus: x => text(x, fill: blue, weight: "bold"), + format-minus: x => strike(text(x, fill: red, size: 0.75em)), + split-regex: "[^A-Za-z0-9]", +) = { + let arr_a = content-text(a).sum() + let arr_b = content-text(b).sum() + + let diff = diff-string-array(arr_a, arr_b, split-regex) + + content-output(b, diff, format-plus, format-minus) +} diff --git a/packages/preview/palimset/0.1.0/diff-function.typ b/packages/preview/palimset/0.1.0/diff-function.typ new file mode 100644 index 0000000000..d780d2a0f9 --- /dev/null +++ b/packages/preview/palimset/0.1.0/diff-function.typ @@ -0,0 +1,214 @@ +#let lcs-getv-status(v_minus, v_plus) = { + if (v_minus.p == none) and (v_plus.p == none) { + return 0 + } + if v_minus.p == none { + return 1 + } + if v_plus.p == none { + return 2 + } + + if (v_minus.x < v_plus.x) { + return 1 + } else { + return 2 + } +} + + +#let lcs-character(a, b) = { + let a_num = a.len() + let b_num = b.len() + let all_num = a_num + b_num + let v = range(all_num + 3).map(n => (x: 0, y: 0, p: none)) + + let offset = b_num + 1 + + for d in range(all_num) { + let k_max = d + let k_min = d + if (d > a_num) { k_max = a_num - (d - a_num) } + if (d > b_num) { k_min = b_num - (d - b_num) } + + for k in range(-k_min, k_max, step: 2) { + let index = offset + k + let x = 0 + let y = 0 + let parent = none + let check = lcs-getv-status(v.at(index - 1), v.at(index + 1)) + + if check == 0 { + x = 0 + y = 0 + parent = (x: 0, y: 0, p: none) + } else if check == 1 { + x = v.at(index + 1).x + y = v.at(index + 1).y + 1 + parent = v.at(index + 1) + } else if check == 2 { + x = v.at(index - 1).x + 1 + y = v.at(index - 1).y + parent = v.at(index - 1) + } + + while (x < a_num) and (y < b_num) and (a.at(x) == b.at(y)) { + x += 1 + y += 1 + } + v.at(index) = (x: x, y: y, p: parent) + + if (a_num <= x) and (b_num <= y) { + return (v.at(index), d) + } + } + } +} + +#let lcs-output(a, b) = { + let tree = lcs-character(a, b) + let output = () + let output_int = () + + let total_d = tree.at(1) + + let now_tree = tree.at(0) + let x = 0 + let y = 0 + let x_bef = 0 + let y_bef = 0 + let match = 0 + let check = 0 + + let a_rev = a.rev() + let b_rev = b.rev() + + for d in range(total_d, 0, step: -1) { + x = now_tree.x + y = now_tree.y + now_tree = now_tree.p + x_bef = now_tree.x + y_bef = now_tree.y + + if (y - y_bef == x - x_bef) { + check = 0 + } else if (y - y_bef > x - x_bef) { + check = 1 + } else { + check = 2 + } + + if (y - y_bef > 0) and (x - x_bef > 0) { + if check == 1 { + match = x - x_bef + } else { + match = y - y_bef + } + } else { + match = 0 + } + + if match != 0 { + output.insert(0, a_rev.slice(0, match).rev()) + output_int.insert(0, 0) + a_rev = a_rev.slice(match, a_rev.len()) + b_rev = b_rev.slice(match, b_rev.len()) + } + + if check == 1 { + output.insert(0, b_rev.first()) + output_int.insert(0, 1) + b_rev = b_rev.slice(1, b_rev.len()) + } + if check == 2 { + output.insert(0, a_rev.first()) + output_int.insert(0, -1) + a_rev = a_rev.slice(1, a_rev.len()) + } + } + + let bef = output_int.at(0) + let str = none + let output2 = () + let output_int2 = () + + for value in range(output_int.len()) { + if output_int.at(value) == bef { + str += output.at(value) + } else { + output2.push(str) + output_int2.push(output_int.at(value - 1)) + str = output.at(value) + bef = output_int.at(value) + } + } + + output2.push(str) + output_int2.push(output_int.last()) + + for value in range(output2.len()) { + let tmp = output2.at(value) + if type(tmp) == array { + output2.at(value) = tmp.sum() + } + } + + return (output2, output_int2) +} + +#let split_words(str, reg) = { + let reg1 = regex(reg) + + let str_arr = str.clusters() + let output_arr = () + let str = "" + + for char in str_arr { + if reg1 in char { + str += char + output_arr.push(str) + str = "" + } else { + str += char + } + } + + if str != "" { + output_arr.push(str) + } + + return output_arr +} + +#let diff-string-array( + a, + b, + reg, +) = { + let a_words = split_words(a, reg) + let b_words = split_words(b, reg) + let output = lcs-output(a_words, b_words) + return output +} + +#let diff-string( + a, + b, + format-plus: x => text(x, fill: blue, weight: "bold"), + format-minus: x => strike(text(x, fill: red, size: 0.75em)), + split-regex: "[^A-Za-z0-9]", +) = { + let data = diff-string-array(a, b, split-regex) + + for value in range(data.at(0).len()) { + let contents = data.at(0).at(value) + let number = data.at(1).at(value) + if number == 0 { + text(contents) + } else if number == 1 { + format-plus(contents) + } else { + format-minus(contents) + } + } +} diff --git a/packages/preview/palimset/0.1.0/example/diff-a.typ b/packages/preview/palimset/0.1.0/example/diff-a.typ new file mode 100644 index 0000000000..371d7f8af3 --- /dev/null +++ b/packages/preview/palimset/0.1.0/example/diff-a.typ @@ -0,0 +1,78 @@ +#import "@preview/charged-ieee:0.1.4": ieee +#import "@preview/cetz:0.4.2" as cetz + +#show: ieee.with( + title: [Complex Document Example], + authors: ( + ( + name: "Example Author", + department: [Department of Examples], + organization: [Example University], + location: [Example City, EX 12345, Country], + email: "diff-doc@example.edu" + ), + ), + abstract: [ + This is an example of a complex document created using the typographic markup language. + It includes sections such as Abstract, Introduction, Numerical Methods, Results, Discussion, and Conclusion, along with mathematical equations and figures. + ], + index-terms: ("Scientific writing", "Typesetting", "Document creation", "Syntax"), +) + += Introduction + +This section serves as an introduction to a completely fictional study. +The purpose of this document is not to address a real scientific problem, but to provide a well-structured example text for testing document comparison tools. +Despite its meaningless content, the writing style follows common conventions found in academic publications. + +The reader is encouraged not to search for physical interpretations or mathematical rigor in the following sections. + + += Example of Mathematical Expressions + +In this section, we present several mathematical expressions that resemble those commonly used in scientific articles. + +A representative equation is given by +$ + f(x) = alpha x^2 + beta x + gamma +$ +where $alpha$, $beta$, and $gamma$ are arbitrary constants with no particular physical meaning. + +Another example involves a summation: +$ + S = sum_(k=1)^N k^2 +$ +which is introduced solely to demonstrate the appearance of inline +and displayed mathematical formulas in a document. + += Example of Figures + +Figures are essential components of technical documents, +even when the data being visualized are entirely artificial. + +#figure( + cetz.canvas({ + import cetz.draw: * + + merge-path({ + for x in range(80).map(n => 0.1*n){ + line((x, calc.sin(x)), (x+0.1, calc.sin(x+0.1))) + } + }) + }), + caption: [figure sample] +) + +@fig-sample illustrates a hypothetical relationship between two variables. +Although the plotted curve appears smooth and well-behaved, it is purely a product of mathematical functions without any empirical basis. + +All figures included in this document should be interpreted +as placeholders rather than meaningful results. + += Conclusion + +This document has presented a fabricated example of an academic-style text. +It included an introduction, mathematical expressions, a description of figures, and a concluding section. + +The primary goal was to create a structured document suitable for testing diff tools and document comparison workflows. +Future revisions may introduce minor textual changes in order to generate visible and instructive differences. diff --git a/packages/preview/palimset/0.1.0/example/diff-b.typ b/packages/preview/palimset/0.1.0/example/diff-b.typ new file mode 100644 index 0000000000..9028815a6c --- /dev/null +++ b/packages/preview/palimset/0.1.0/example/diff-b.typ @@ -0,0 +1,78 @@ +#import "@preview/charged-ieee:0.1.4": ieee +#import "@preview/cetz:0.4.2" as cetz + +#show: ieee.with( + title: [Complex Document Example], + authors: ( + ( + name: "Example Author", + department: [Department of Examples], + organization: [Example University], + location: [Example City, EX 12345, Country], + email: "diff-doc@example.edu" + ), + ), + abstract: [ + This is an example of a complex document created using the typographic markup language. + It includes sections such as Abstract, Introduction, Numerical Methods, Results, Discussion, and Conclusion, along with mathematical equations and figures. + ], + index-terms: ("Scientific writing", "Typesetting", "Document creation", "Syntax"), +) + += Introduction + +This section serves as an introduction to a largely fictional study. +The purpose of this document is not to address a real scientific problem, but to provide a clear and well-structured example text for testing document comparison tools. +Despite its meaningless content, the writing style follows common conventions found in academic publications. + +The reader is encouraged not to search for physical interpretations or deep mathematical rigor in the following sections. + + += Example of Mathematical Expressions + +In this section, we present several mathematical expressions that resemble those commonly used in scientific articles. + +A representative equation is given by +$ + f(x) = alpha x^2 + beta x + gamma +$ +where $alpha$, $beta$, and $gamma$ are arbitrary constants with no particular physical meaning. + +Another example involves a summation: +$ + S = sum_(k=1)^N k^2 +$ +which is introduced solely to demonstrate the appearance of inline +and displayed mathematical formulas in a document. + += Example of Figures + +Figures are essential components of technical documents, +even when the data being visualized are entirely artificial. + +#figure( + cetz.canvas({ + import cetz.draw: * + + merge-path({ + for x in range(80).map(n => 0.1*n){ + line((x, calc.sin(x)), (x+0.1, calc.sin(x+0.1))) + } + }) + }), + caption: [figure sample] +) + +@fig-sample illustrates a hypothetical relationship between two variables. +Although the plotted curve appears smooth and well-behaved, it is generated using simple mathematical functions without any empirical basis. + +All figures included in this document should be interpreted +as placeholders rather than meaningful results. + += Conclusion + +This document has presented a fabricated example of an academic-style text. +It included an introduction, mathematical expressions, a brief description of figures, and a concluding section. + +The primary goal was to create a structured document suitable for testing diff tools and document comparison workflows. +Future revisions may introduce small textual changes in order to generate visible and instructive differences. diff --git a/packages/preview/palimset/0.1.0/example/diff-content.png b/packages/preview/palimset/0.1.0/example/diff-content.png new file mode 100644 index 0000000000..bb1d61e6fc Binary files /dev/null and b/packages/preview/palimset/0.1.0/example/diff-content.png differ diff --git a/packages/preview/palimset/0.1.0/example/diff-string.png b/packages/preview/palimset/0.1.0/example/diff-string.png new file mode 100644 index 0000000000..1eb3f63e3e Binary files /dev/null and b/packages/preview/palimset/0.1.0/example/diff-string.png differ diff --git a/packages/preview/palimset/0.1.0/example/diff-string.typ b/packages/preview/palimset/0.1.0/example/diff-string.typ new file mode 100644 index 0000000000..426ee9ab6b --- /dev/null +++ b/packages/preview/palimset/0.1.0/example/diff-string.typ @@ -0,0 +1,9 @@ +#import "@preview/js:0.1.3": * +#import "@preview/palimset:0.1.0": * + +#show: js + +#let a = "hello, workd. こんばんは" +#let b = "hello, world. こんにちは" + +#diff-string(a, b) diff --git a/packages/preview/palimset/0.1.0/example/diff_content.typ b/packages/preview/palimset/0.1.0/example/diff_content.typ new file mode 100644 index 0000000000..394262f64e --- /dev/null +++ b/packages/preview/palimset/0.1.0/example/diff_content.typ @@ -0,0 +1,6 @@ +#import "@preview/palimset:0.1.0": * + +#diff-content( + include "diff-a.typ", + include "diff-b.typ" +) diff --git a/packages/preview/palimset/0.1.0/lib.typ b/packages/preview/palimset/0.1.0/lib.typ new file mode 100644 index 0000000000..52877f6efb --- /dev/null +++ b/packages/preview/palimset/0.1.0/lib.typ @@ -0,0 +1,2 @@ +#import "diff-function.typ": diff-string +#import "diff-content.typ": diff-content diff --git a/packages/preview/palimset/0.1.0/typst.toml b/packages/preview/palimset/0.1.0/typst.toml new file mode 100644 index 0000000000..76b9b3098b --- /dev/null +++ b/packages/preview/palimset/0.1.0/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "palimset" +version = "0.1.0" +entrypoint = "lib.typ" +authors = ["Takeru Hashimoto"] +license = "MIT" +description = "A package that shows the differences between two documents." +repository = "https://github.com/tkrhsmt/palimset" +exclude = ["example/"]