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/"]