From 7d7decb459f0b84382366b314984702dcc7442aa Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:03:43 +0000 Subject: [PATCH] highlights --- .../page-history/components/changeset.md | 145 ++++++++++++++++++ .../components/history-editor.tsx | 6 +- .../components/history-modal-body.tsx | 47 ++++-- .../page-history/components/history-view.tsx | 4 +- 4 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 apps/client/src/features/page-history/components/changeset.md diff --git a/apps/client/src/features/page-history/components/changeset.md b/apps/client/src/features/page-history/components/changeset.md new file mode 100644 index 00000000..0d9ea39d --- /dev/null +++ b/apps/client/src/features/page-history/components/changeset.md @@ -0,0 +1,145 @@ +# prosemirror-changeset + +This is a helper module that can turn a sequence of document changes +into a set of insertions and deletions, for example to display them in +a change-tracking interface. Such a set can be built up incrementally, +in order to do such change tracking in a halfway performant way during +live editing. + +This code is licensed under an [MIT +licence](https://github.com/ProseMirror/prosemirror-changeset/blob/master/LICENSE). + +## Programming interface + +Insertions and deletions are represented as ‘spans’—ranges in the +document. The deleted spans refer to the original document, whereas +the inserted ones point into the current document. + +It is possible to associate arbitrary data values with such spans, for +example to track the user that made the change, the timestamp at which +it was made, or the step data necessary to invert it again. + +### class Change`` + +A replaced range with metadata associated with it. + +* **`fromA`**`: number`\ + The start of the range deleted/replaced in the old document. + +* **`toA`**`: number`\ + The end of the range in the old document. + +* **`fromB`**`: number`\ + The start of the range inserted in the new document. + +* **`toB`**`: number`\ + The end of the range in the new document. + +* **`deleted`**`: readonly Span[]`\ + Data associated with the deleted content. The length of these + spans adds up to `this.toA - this.fromA`. + +* **`inserted`**`: readonly Span[]`\ + Data associated with the inserted content. Length adds up to + `this.toB - this.fromB`. + +* `static `**`merge`**`(x: readonly Change[], y: readonly Change[], combine: fn(dataA: Data, dataB: Data) → Data) → readonly Change[]`\ + This merges two changesets (the end document of x should be the + start document of y) into a single one spanning the start of x to + the end of y. + + +### class Span`` + +Stores metadata for a part of a change. + +* **`length`**`: number`\ + The length of this span. + +* **`data`**`: Data`\ + The data associated with this span. + + +### class ChangeSet`` + +A change set tracks the changes to a document from a given point +in the past. It condenses a number of step maps down to a flat +sequence of replacements, and simplifies replacments that +partially undo themselves by comparing their content. + +* **`changes`**`: readonly Change[]`\ + Replaced regions. + +* **`addSteps`**`(newDoc: Node, maps: readonly StepMap[], data: Data | readonly Data[]) → ChangeSet`\ + Computes a new changeset by adding the given step maps and + metadata (either as an array, per-map, or as a single value to be + associated with all maps) to the current set. Will not mutate the + old set. + + Note that due to simplification that happens after each add, + incrementally adding steps might create a different final set + than adding all those changes at once, since different document + tokens might be matched during simplification depending on the + boundaries of the current changed ranges. + +* **`startDoc`**`: Node`\ + The starting document of the change set. + +* **`map`**`(f: fn(range: Span) → Data) → ChangeSet`\ + Map the span's data values in the given set through a function + and construct a new set with the resulting data. + +* **`changedRange`**`(b: ChangeSet, maps?: readonly StepMap[]) → {from: number, to: number}`\ + Compare two changesets and return the range in which they are + changed, if any. If the document changed between the maps, pass + the maps for the steps that changed it as second argument, and + make sure the method is called on the old set and passed the new + set. The returned positions will be in new document coordinates. + +* `static `**`create`**`(doc: Node, combine?: fn(dataA: Data, dataB: Data) → Data = (a, b) => a === b ? a : null as any, tokenEncoder?: TokenEncoder = DefaultEncoder) → ChangeSet`\ + Create a changeset with the given base object and configuration. + + The `combine` function is used to compare and combine metadata—it + should return null when metadata isn't compatible, and a combined + version for a merged range when it is. + + When given, a token encoder determines how document tokens are + serialized and compared when diffing the content produced by + changes. The default is to just compare nodes by name and text + by character, ignoring marks and attributes. + + +* **`simplifyChanges`**`(changes: readonly Change[], doc: Node) → Change[]`\ + Simplifies a set of changes for presentation. This makes the + assumption that having both insertions and deletions within a word + is confusing, and, when such changes occur without a word boundary + between them, they should be expanded to cover the entire set of + words (in the new document) they touch. An exception is made for + single-character replacements. + + +### interface TokenEncoder`` + +A token encoder can be passed when creating a `ChangeSet` in order +to influence the way the library runs its diffing algorithm. The +encoder determines how document tokens (such as nodes and +characters) are encoded and compared. + +Note that both the encoding and the comparison may run a lot, and +doing non-trivial work in these functions could impact +performance. + +* **`encodeCharacter`**`(char: number, marks: readonly Mark[]) → T`\ + Encode a given character, with the given marks applied. + +* **`encodeNodeStart`**`(node: Node) → T`\ + Encode the start of a node or, if this is a leaf node, the + entire node. + +* **`encodeNodeEnd`**`(node: Node) → T`\ + Encode the end token for the given node. It is valid to encode + every end token in the same way. + +* **`compareTokens`**`(a: T, b: T) → boolean`\ + Compare the given tokens. Should return true when they count as + equal. diff --git a/apps/client/src/features/page-history/components/history-editor.tsx b/apps/client/src/features/page-history/components/history-editor.tsx index 6aea8859..dc21a82b 100644 --- a/apps/client/src/features/page-history/components/history-editor.tsx +++ b/apps/client/src/features/page-history/components/history-editor.tsx @@ -14,12 +14,14 @@ export interface HistoryEditorProps { title: string; content: any; previousContent?: any; + highlightChanges?: boolean; } export function HistoryEditor({ title, content, previousContent, + highlightChanges = true, }: HistoryEditorProps) { const editor = useEditor({ extensions: mainExtensions, @@ -102,10 +104,10 @@ export function HistoryEditor({ editor.setOptions({ editorProps: { ...editor.options.editorProps, - decorations: () => decorationSet, + decorations: () => (highlightChanges ? decorationSet : DecorationSet.empty), }, }); - }, [title, content, editor, previousContent]); + }, [title, content, editor, previousContent, highlightChanges]); return ( <> diff --git a/apps/client/src/features/page-history/components/history-modal-body.tsx b/apps/client/src/features/page-history/components/history-modal-body.tsx index c106db0a..2724308b 100644 --- a/apps/client/src/features/page-history/components/history-modal-body.tsx +++ b/apps/client/src/features/page-history/components/history-modal-body.tsx @@ -1,4 +1,4 @@ -import { ScrollArea } from "@mantine/core"; +import { Paper, ScrollArea, Switch } from "@mantine/core"; import HistoryList from "@/features/page-history/components/history-list"; import classes from "./history.module.css"; import { useAtom } from "jotai"; @@ -7,7 +7,7 @@ import { activeHistoryPrevIdAtom, } from "@/features/page-history/atoms/history-atoms"; import HistoryView from "@/features/page-history/components/history-view"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; interface Props { pageId: string; @@ -18,6 +18,7 @@ export default function HistoryModalBody({ pageId }: Props) { const [activeHistoryPrevId, setActiveHistoryPrevId] = useAtom( activeHistoryPrevIdAtom, ); + const [highlightChanges, setHighlightChanges] = useState(true); useEffect(() => { setActiveHistoryId(""); @@ -32,16 +33,40 @@ export default function HistoryModalBody({ pageId }: Props) { - -
- {activeHistoryId && ( - + +
+ {activeHistoryId && ( + + )} +
+
+ + {activeHistoryId && activeHistoryPrevId && ( + + setHighlightChanges(e.currentTarget.checked)} /> - )} -
-
+ + )} + ); } diff --git a/apps/client/src/features/page-history/components/history-view.tsx b/apps/client/src/features/page-history/components/history-view.tsx index 1579d949..1a265757 100644 --- a/apps/client/src/features/page-history/components/history-view.tsx +++ b/apps/client/src/features/page-history/components/history-view.tsx @@ -5,9 +5,10 @@ import { useTranslation } from "react-i18next"; interface HistoryProps { historyId: string; prevHistoryId?: string; + highlightChanges?: boolean; } -function HistoryView({ historyId, prevHistoryId }: HistoryProps) { +function HistoryView({ historyId, prevHistoryId, highlightChanges }: HistoryProps) { const { t } = useTranslation(); const { data, @@ -35,6 +36,7 @@ function HistoryView({ historyId, prevHistoryId }: HistoryProps) { content={data.content} title={data.title} previousContent={!isErrorPrev ? prevData?.content : undefined} + highlightChanges={highlightChanges} /> )