@tangle-network/ui 11.33.0 → 11.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/editor/document-editor-pane.d.ts +7 -1
  3. package/dist/editor/document-editor-pane.js +27 -12
  4. package/dist/editor/markdown-document-editor.js +30 -12
  5. package/dist/editor/source-markdown-editor.js +256 -0
  6. package/dist/editor/source-markdown.js +495 -0
  7. package/dist/files/file-artifact-pane.d.ts +2 -0
  8. package/dist/files/file-artifact-pane.js +1 -0
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +2 -2
  11. package/dist/markdown/markdown-table.js +1 -1
  12. package/dist/primitives/segmented-control.d.ts +6 -3
  13. package/dist/primitives/segmented-control.js +105 -56
  14. package/dist/run/inline-thinking-item.d.ts +7 -1
  15. package/dist/run/inline-thinking-item.js +6 -5
  16. package/dist/utils/format.d.ts +6 -0
  17. package/dist/utils/format.js +9 -1
  18. package/dist/utils.d.ts +2 -2
  19. package/dist/utils.js +2 -2
  20. package/package.json +2 -2
  21. package/src/editor/document-editor-pane.tsx +54 -16
  22. package/src/editor/markdown-document-editor.tsx +33 -18
  23. package/src/editor/source-markdown-editor.test.tsx +156 -0
  24. package/src/editor/source-markdown-editor.ts +296 -0
  25. package/src/editor/source-markdown.fixtures.ts +90 -0
  26. package/src/editor/source-markdown.test.ts +235 -0
  27. package/src/editor/source-markdown.ts +627 -0
  28. package/src/files/file-artifact-pane.test.tsx +34 -1
  29. package/src/files/file-artifact-pane.tsx +3 -0
  30. package/src/markdown/markdown-table.test.tsx +9 -0
  31. package/src/markdown/markdown-table.tsx +5 -1
  32. package/src/primitives/segmented-control.stories.tsx +89 -0
  33. package/src/primitives/segmented-control.test.tsx +20 -7
  34. package/src/primitives/segmented-control.tsx +90 -29
  35. package/src/run/inline-thinking-item.test.tsx +29 -0
  36. package/src/run/inline-thinking-item.tsx +11 -4
  37. package/src/utils/format.test.ts +16 -1
  38. package/src/utils/format.ts +17 -0
  39. package/src/utils/index.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.35.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 19e3cd6: SegmentedControl: a strip whose options outgrow it now shows a chevron at each edge with options past it, instead of an edge fade that a host's inline padding or a cut between two options made invisible. The chevrons page the strip on click or tap and take no room in the row. The selected option is kept fully in view clear of the chevrons, and this now also works for a row control a host made scroll with `flex-nowrap overflow-x-auto`.
8
+
9
+ ## 11.34.0
10
+
11
+ ### Minor Changes
12
+
13
+ - d81b6ec: The rich Markdown editor writes back the file's own bytes. Opening a file and leaving it reports no change; an edit changes only the edited span, keeping list markers, emphasis delimiters, escapes, setext headings, reference links and blank lines as written. Blocks the editor has no node for (GFM tables, raw HTML, images, task lists, link definitions) are shown read-only and kept verbatim, so opening Edit no longer flattens a table into one paragraph.
14
+
15
+ `DocumentEditorPane` and `FileArtifactPane`'s `editor` option take `dirty`, so a host that tracks unsaved edits itself drives the save state and the Save button. The pane shows one save state at 14px ("Unsaved changes", "Saving…" or "Saved"), shows Save only while there is something to save, and saves on Ctrl+S / Cmd+S.
16
+
17
+ ### Patch Changes
18
+
19
+ - 0b915b2: Markdown tables contain their copy control's screen-reader status line. The card is now `relative`, so a table deep inside a scrolling panel no longer stretches the page past the viewport and lets a wheel scroll the whole app away.
20
+ - 2d96408: InlineThinkingItem previews reasoning as plain text (no `**` or `#` marks), and takes `openWhileActive` (default true) so a product can keep a live reasoning row closed until the reader opens it. Adds `plainTextFromMarkdown` to the utils.
21
+
3
22
  ## 11.33.0
4
23
 
5
24
  ### Minor Changes
@@ -12,6 +12,12 @@ export interface DocumentEditorPaneProps extends Omit<ArtifactPaneProps, "childr
12
12
  markdown?: string;
13
13
  /** Persisted baseline when the host controls the current markdown draft. */
14
14
  savedMarkdown?: string;
15
+ /**
16
+ * Whether the draft has unsaved edits, when the host tracks that itself
17
+ * (for example against the file's full bytes, frontmatter included). It
18
+ * takes precedence over comparing the draft with `savedMarkdown`.
19
+ */
20
+ dirty?: boolean;
15
21
  /** Transform Markdown links and images through the shared URL policy. */
16
22
  urlTransform?: UrlTransform;
17
23
  mode?: DocumentEditorMode;
@@ -33,5 +39,5 @@ export interface DocumentEditorPaneProps extends Omit<ArtifactPaneProps, "childr
33
39
  * DocumentEditorPane — reusable markdown document surface with preview/edit
34
40
  * modes and optional collaborative editing backed by Yjs/Hocuspocus.
35
41
  */
36
- export declare function DocumentEditorPane({ eyebrow, title, subtitle, meta, headerActions, footer, className, contentClassName, headerClassName, hideTitleBlock, tabs, toolbar, markdown, savedMarkdown, urlTransform, mode, defaultMode, onModeChange, backend, placeholder, autoFocus, readOnly, onChange, onSave, saving, saveLabel, previewClassName, editorClassName, collaboration }: DocumentEditorPaneProps): import("react").JSX.Element;
42
+ export declare function DocumentEditorPane({ eyebrow, title, subtitle, meta, headerActions, footer, className, contentClassName, headerClassName, hideTitleBlock, tabs, toolbar, markdown, savedMarkdown, dirty, urlTransform, mode, defaultMode, onModeChange, backend, placeholder, autoFocus, readOnly, onChange, onSave, saving, saveLabel, previewClassName, editorClassName, collaboration }: DocumentEditorPaneProps): import("react").JSX.Element;
37
43
  //#endregion
@@ -79,13 +79,24 @@ function CollaborativeDocumentSurface({ markdown, placeholder, autoFocus, readOn
79
79
  * DocumentEditorPane — reusable markdown document surface with preview/edit
80
80
  * modes and optional collaborative editing backed by Yjs/Hocuspocus.
81
81
  */
82
- function DocumentEditorPane({ eyebrow, title, subtitle, meta, headerActions, footer, className, contentClassName, headerClassName, hideTitleBlock, tabs, toolbar, markdown = "", savedMarkdown, urlTransform, mode, defaultMode = "preview", onModeChange, backend = "local", placeholder = "Start writing...", autoFocus = false, readOnly = false, onChange, onSave, saving = false, saveLabel = "Save changes", previewClassName, editorClassName, collaboration }) {
82
+ function DocumentEditorPane({ eyebrow, title, subtitle, meta, headerActions, footer, className, contentClassName, headerClassName, hideTitleBlock, tabs, toolbar, markdown = "", savedMarkdown, dirty, urlTransform, mode, defaultMode = "preview", onModeChange, backend = "local", placeholder = "Start writing...", autoFocus = false, readOnly = false, onChange, onSave, saving = false, saveLabel = "Save changes", previewClassName, editorClassName, collaboration }) {
83
83
  const [draft, setDraft] = useState(markdown);
84
84
  const [uncontrolledMode, setUncontrolledMode] = useState(defaultMode);
85
85
  const activeMode = mode ?? uncontrolledMode;
86
86
  const isCollaborative = backend === "collaborative" && Boolean(collaboration);
87
- const isDirty = normalizeMarkdown(draft) !== normalizeMarkdown(savedMarkdown ?? markdown);
88
- const saveStateLabel = readOnly ? "Read only" : isCollaborative ? isDirty ? "Snapshot pending" : "Live document current" : isDirty ? "Unsaved changes" : "Saved";
87
+ const isDirty = dirty ?? normalizeMarkdown(draft) !== normalizeMarkdown(savedMarkdown ?? markdown);
88
+ const canSave = Boolean(onSave) && !readOnly && isDirty && !saving;
89
+ const saveStateLabel = readOnly ? "Read only" : isCollaborative ? isDirty ? "Snapshot pending" : "Live document current" : saving ? "Saving…" : isDirty ? "Unsaved changes" : "Saved";
90
+ const save = () => {
91
+ if (canSave) onSave?.(draft);
92
+ };
93
+ const handleKeyDown = (event) => {
94
+ if (!onSave || readOnly) return;
95
+ if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
96
+ if (event.key.toLowerCase() !== "s") return;
97
+ event.preventDefault();
98
+ save();
99
+ };
89
100
  useEffect(() => {
90
101
  setDraft(markdown);
91
102
  }, [markdown]);
@@ -117,23 +128,26 @@ function DocumentEditorPane({ eyebrow, title, subtitle, meta, headerActions, foo
117
128
  children: [/* @__PURE__ */ jsx(PencilLine, { className: "h-3.5 w-3.5" }), isCollaborative ? "Live edit" : "Edit"]
118
129
  })]
119
130
  }), /* @__PURE__ */ jsxs("div", {
120
- className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground",
131
+ className: "flex flex-wrap items-center gap-2 text-sm text-muted-foreground",
121
132
  children: [
122
133
  toolbar,
123
- /* @__PURE__ */ jsx("span", {
134
+ isCollaborative && /* @__PURE__ */ jsx("span", {
124
135
  className: "rounded-full border border-border bg-card px-2.5 py-1 font-medium",
125
- children: isCollaborative ? "Live document" : "Local draft"
136
+ children: "Live document"
126
137
  }),
127
138
  /* @__PURE__ */ jsx("span", {
128
- className: "rounded-full border border-border bg-background px-2.5 py-1",
139
+ role: "status",
140
+ "aria-live": "polite",
141
+ className: cn("rounded-full border px-2.5 py-1", isDirty && !readOnly ? "border-[var(--surface-warning-border)] bg-[var(--surface-warning-bg)] text-[var(--surface-warning-text)]" : "border-border bg-background"),
129
142
  children: saveStateLabel
130
143
  }),
131
- onSave && !readOnly && /* @__PURE__ */ jsxs("button", {
144
+ onSave && !readOnly && (isDirty || saving) && /* @__PURE__ */ jsxs("button", {
132
145
  type: "button",
133
- onClick: () => void onSave(draft),
134
- disabled: saving || !isDirty,
135
- className: cn("inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-border bg-card px-3 py-1.5 text-xs font-semibold text-foreground transition-colors hover:border-[var(--border-strong)] hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50", focusRing),
136
- children: [/* @__PURE__ */ jsx(Save, { className: "h-3.5 w-3.5" }), saving ? "Saving..." : saveLabel]
146
+ onClick: save,
147
+ disabled: !canSave,
148
+ title: "Save changes (Ctrl+S)",
149
+ className: cn("inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-primary bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50", focusRing),
150
+ children: [/* @__PURE__ */ jsx(Save, { className: "h-4 w-4" }), saveLabel]
137
151
  })
138
152
  ]
139
153
  })]
@@ -169,6 +183,7 @@ function DocumentEditorPane({ eyebrow, title, subtitle, meta, headerActions, foo
169
183
  return /* @__PURE__ */ jsx(Tabs, {
170
184
  value: activeMode,
171
185
  onValueChange: (nextValue) => setMode(nextValue),
186
+ onKeyDown: handleKeyDown,
172
187
  className: "h-full",
173
188
  children: /* @__PURE__ */ jsxs(ArtifactPane, {
174
189
  eyebrow,
@@ -4,7 +4,8 @@ import { loadDocumentEditorPeers } from "./editor-peers.js";
4
4
  import { retryableLazyEditor } from "./editor-lazy.js";
5
5
  import { EditorLoadingPlaceholder } from "./editor-loading.js";
6
6
  import { EditorToolbar } from "./editor-toolbar.js";
7
- import { htmlToMarkdown, markdownToHtml, normalizeMarkdown } from "./markdown-conversion.js";
7
+ import { normalizeMarkdown } from "./markdown-conversion.js";
8
+ import { SourceMarkdownSession, createSourceMarkdownExtensions } from "./source-markdown-editor.js";
8
9
  import { Suspense, useEffect, useMemo, useRef } from "react";
9
10
  import { jsx, jsxs } from "react/jsx-runtime";
10
11
  //#region src/editor/markdown-document-editor.tsx
@@ -12,15 +13,28 @@ import { jsx, jsxs } from "react/jsx-runtime";
12
13
  * Builds the local markdown editor against loaded tiptap namespaces. The
13
14
  * peers arrive as an argument so that this module reaches them through
14
15
  * type-only imports, which a bundler erases.
16
+ *
17
+ * The editor writes back the file's own bytes wherever the reader changed
18
+ * nothing (see `source-markdown.ts`): opening a file and leaving it reports no
19
+ * change, and an edit changes only the edited span. Blocks it cannot represent,
20
+ * such as tables, are shown read-only and kept verbatim.
15
21
  */
16
22
  function createMarkdownDocumentEditor(peers) {
17
23
  const { EditorContent, useEditor } = peers.react;
18
24
  const StarterKit = peers.starterKit.default;
25
+ const sourceExtensions = createSourceMarkdownExtensions(peers.react);
19
26
  return function MarkdownDocumentEditor({ value = "", placeholder = "Start writing...", readOnly = false, autoFocus = false, className, contentClassName, onChange, onReady }) {
20
- const initialHtml = useMemo(() => markdownToHtml(value), []);
21
- const lastAppliedMarkdownRef = useRef(normalizeMarkdown(value));
27
+ const sessionRef = useRef(null);
28
+ if (!sessionRef.current) sessionRef.current = new SourceMarkdownSession(peers.react, value);
29
+ const initialHtml = useMemo(() => sessionRef.current.html, []);
30
+ const lastMarkdownRef = useRef(value);
31
+ const onChangeRef = useRef(onChange);
32
+ onChangeRef.current = onChange;
22
33
  const editor = useEditor({
23
- extensions: [StarterKit.configure({ codeBlock: { HTMLAttributes: { class: "hljs" } } })],
34
+ extensions: [StarterKit.configure({
35
+ codeBlock: { HTMLAttributes: { class: "hljs" } },
36
+ underline: false
37
+ }), ...sourceExtensions],
24
38
  content: initialHtml,
25
39
  editable: !readOnly,
26
40
  autofocus: autoFocus,
@@ -29,23 +43,27 @@ function createMarkdownDocumentEditor(peers) {
29
43
  "data-placeholder": placeholder
30
44
  } },
31
45
  onUpdate: ({ editor: currentEditor }) => {
32
- const nextMarkdown = normalizeMarkdown(htmlToMarkdown(currentEditor.getHTML()));
33
- lastAppliedMarkdownRef.current = nextMarkdown;
34
- onChange?.(nextMarkdown, currentEditor);
46
+ const nextMarkdown = sessionRef.current.serialize(currentEditor.state.doc);
47
+ if (nextMarkdown === lastMarkdownRef.current) return;
48
+ lastMarkdownRef.current = nextMarkdown;
49
+ onChangeRef.current?.(nextMarkdown, currentEditor);
35
50
  },
36
51
  onCreate: ({ editor: currentEditor }) => {
52
+ sessionRef.current.attach(currentEditor);
37
53
  onReady?.(currentEditor);
38
54
  }
39
55
  });
40
56
  useEffect(() => {
41
- if (editor) editor.setEditable(!readOnly);
57
+ editor?.setEditable(!readOnly, false);
42
58
  }, [editor, readOnly]);
43
59
  useEffect(() => {
44
60
  if (!editor) return;
45
- const normalizedValue = normalizeMarkdown(value);
46
- if (normalizedValue === lastAppliedMarkdownRef.current) return;
47
- editor.commands.setContent(markdownToHtml(value), { emitUpdate: false });
48
- lastAppliedMarkdownRef.current = normalizedValue;
61
+ if (normalizeMarkdown(value) === normalizeMarkdown(lastMarkdownRef.current)) return;
62
+ const session = new SourceMarkdownSession(peers.react, value);
63
+ sessionRef.current = session;
64
+ editor.commands.setContent(session.html, { emitUpdate: false });
65
+ session.attach(editor);
66
+ lastMarkdownRef.current = value;
49
67
  }, [editor, value]);
50
68
  return /* @__PURE__ */ jsxs("div", {
51
69
  className: cn("flex min-h-[14rem] w-full flex-col overflow-hidden rounded-lg border border-border bg-background", className),
@@ -0,0 +1,256 @@
1
+ import { BLOCK_ID_ATTRIBUTE, SOURCE_KIND_ATTRIBUTE, SOURCE_RAW_ATTRIBUTE, asReadOnlyDocument, blockHtml, blockMarkdown, createBlockMerger, parseSourceMarkdown, serializeSourceMarkdown, withReadOnlyBlocks } from "./source-markdown.js";
2
+ import { marked } from "marked";
3
+ //#region src/editor/source-markdown-editor.ts
4
+ /** Name of the read-only node that holds a block's raw Markdown. */
5
+ const SOURCE_NODE = "markdownSource";
6
+ /** Top-level node types that can carry a block id. */
7
+ const BLOCK_TYPES = [
8
+ "paragraph",
9
+ "heading",
10
+ "bulletList",
11
+ "orderedList",
12
+ "blockquote",
13
+ "codeBlock",
14
+ "horizontalRule",
15
+ SOURCE_NODE
16
+ ];
17
+ const CAPTIONS = {
18
+ table: "Table",
19
+ html: "HTML",
20
+ image: "Image",
21
+ checklist: "Checklist",
22
+ definition: "Link definition",
23
+ markdown: "Markdown",
24
+ paragraph: "Markdown",
25
+ heading: "Markdown",
26
+ code: "Markdown",
27
+ blockquote: "Markdown",
28
+ list: "Markdown",
29
+ hr: "Markdown"
30
+ };
31
+ function inlineText(tokens) {
32
+ return (tokens ?? []).map((token) => {
33
+ switch (token.type) {
34
+ case "image": return token.text;
35
+ case "br": return " ";
36
+ case "html": return "";
37
+ default: {
38
+ const children = token.tokens;
39
+ return children ? inlineText(children) : token.text ?? "";
40
+ }
41
+ }
42
+ }).join("");
43
+ }
44
+ function tablePreview(raw) {
45
+ const table = marked.lexer(raw, { gfm: true }).find((token) => token.type === "table");
46
+ if (!table) return null;
47
+ const cell = (tag, value) => [
48
+ tag,
49
+ {},
50
+ inlineText(value.tokens)
51
+ ];
52
+ return [
53
+ "table",
54
+ {},
55
+ [
56
+ "thead",
57
+ {},
58
+ [
59
+ "tr",
60
+ {},
61
+ ...table.header.map((value) => cell("th", value))
62
+ ]
63
+ ],
64
+ [
65
+ "tbody",
66
+ {},
67
+ ...table.rows.map((row) => [
68
+ "tr",
69
+ {},
70
+ ...row.map((value) => cell("td", value))
71
+ ])
72
+ ]
73
+ ];
74
+ }
75
+ function sourcePreview(raw, kind) {
76
+ const caption = [
77
+ "p",
78
+ { class: "not-prose m-0 mb-2 text-sm text-muted-foreground" },
79
+ `${CAPTIONS[kind] ?? "Markdown"} kept exactly as written. Edit it in the Markdown source.`
80
+ ];
81
+ const table = kind === "table" ? tablePreview(raw) : null;
82
+ if (table) return [caption, [
83
+ "div",
84
+ { class: "overflow-x-auto" },
85
+ table
86
+ ]];
87
+ return [caption, [
88
+ "pre",
89
+ { class: "m-0 whitespace-pre-wrap" },
90
+ [
91
+ "code",
92
+ {},
93
+ raw
94
+ ]
95
+ ]];
96
+ }
97
+ /** The read-only source node and the block-id attribute, built from the loaded tiptap. */
98
+ function createSourceMarkdownExtensions(core) {
99
+ return [core.Node.create({
100
+ name: SOURCE_NODE,
101
+ group: "block",
102
+ atom: true,
103
+ selectable: true,
104
+ draggable: false,
105
+ addAttributes() {
106
+ return {
107
+ raw: {
108
+ default: "",
109
+ parseHTML: (element) => element.getAttribute("data-md-raw") ?? "",
110
+ renderHTML: (attributes) => ({ [SOURCE_RAW_ATTRIBUTE]: attributes.raw })
111
+ },
112
+ kind: {
113
+ default: "markdown",
114
+ parseHTML: (element) => element.getAttribute("data-md-kind") ?? "markdown",
115
+ renderHTML: (attributes) => ({ [SOURCE_KIND_ATTRIBUTE]: attributes.kind })
116
+ }
117
+ };
118
+ },
119
+ parseHTML() {
120
+ return [{ tag: `div[${SOURCE_RAW_ATTRIBUTE}]` }];
121
+ },
122
+ renderHTML({ node, HTMLAttributes }) {
123
+ return [
124
+ "div",
125
+ core.mergeAttributes(HTMLAttributes, { class: "my-3 rounded-lg border border-dashed border-border bg-muted/40 p-3" }),
126
+ ...sourcePreview(String(node.attrs.raw ?? ""), node.attrs.kind)
127
+ ];
128
+ }
129
+ }), core.Extension.create({
130
+ name: "markdownSourceBlockId",
131
+ addGlobalAttributes() {
132
+ return [{
133
+ types: BLOCK_TYPES,
134
+ attributes: { mdBlock: {
135
+ default: null,
136
+ keepOnSplit: false,
137
+ parseHTML: (element) => element.getAttribute(BLOCK_ID_ATTRIBUTE),
138
+ renderHTML: (attributes) => attributes.mdBlock ? { [BLOCK_ID_ATTRIBUTE]: attributes.mdBlock } : {}
139
+ } }
140
+ }];
141
+ }
142
+ })];
143
+ }
144
+ function blockIdOf(node) {
145
+ const id = node.attrs.mdBlock;
146
+ return typeof id === "string" && id ? id : null;
147
+ }
148
+ function isEmptyParagraph(node) {
149
+ return node.type.name === "paragraph" && node.content.size === 0;
150
+ }
151
+ function withoutBlockIds(value) {
152
+ if (Array.isArray(value)) return value.map(withoutBlockIds);
153
+ if (!value || typeof value !== "object") return value;
154
+ const out = {};
155
+ for (const [key, entry] of Object.entries(value)) {
156
+ if (key === "mdBlock") continue;
157
+ out[key] = withoutBlockIds(entry);
158
+ }
159
+ return out;
160
+ }
161
+ /**
162
+ * One open file in the rich editor: its source blocks, the nodes they were
163
+ * first parsed into, and the Markdown written back for the current document.
164
+ */
165
+ var SourceMarkdownSession = class {
166
+ source;
167
+ core;
168
+ markdown;
169
+ initial = /* @__PURE__ */ new Map();
170
+ mergers = /* @__PURE__ */ new Map();
171
+ constructor(core, markdown) {
172
+ this.core = core;
173
+ this.markdown = markdown;
174
+ this.source = parseSourceMarkdown(markdown);
175
+ }
176
+ get html() {
177
+ return this.source.html;
178
+ }
179
+ /**
180
+ * Records the nodes the editor parsed. A block the editor document does not
181
+ * hold as exactly one top-level node (dropped, split, or merged by the
182
+ * schema) is shown read-only instead, so no source byte can be lost.
183
+ */
184
+ attach(editor) {
185
+ for (let attempt = 0; attempt < 3; attempt++) {
186
+ const unmapped = this.capture(editor.state.doc);
187
+ if (unmapped.size === 0) return;
188
+ this.source = attempt < 2 ? withReadOnlyBlocks(this.source, unmapped) : asReadOnlyDocument(this.markdown);
189
+ editor.commands.setContent(this.source.html, { emitUpdate: false });
190
+ }
191
+ this.capture(editor.state.doc);
192
+ }
193
+ capture(doc) {
194
+ this.initial.clear();
195
+ this.mergers.clear();
196
+ const unmapped = /* @__PURE__ */ new Set();
197
+ let previous = null;
198
+ doc.forEach((node, _offset, index) => {
199
+ const id = blockIdOf(node);
200
+ if (id && this.source.byId.has(id)) {
201
+ if (this.initial.has(id)) unmapped.add(id);
202
+ else this.initial.set(id, node);
203
+ previous = id;
204
+ return;
205
+ }
206
+ if (index === doc.childCount - 1 && isEmptyParagraph(node)) return;
207
+ const owner = previous ?? this.source.blocks[0]?.id;
208
+ if (owner) unmapped.add(owner);
209
+ });
210
+ for (const block of this.source.blocks) if (!this.initial.has(block.id)) unmapped.add(block.id);
211
+ return unmapped;
212
+ }
213
+ toMarkdown(node, raw) {
214
+ const fragment = node.type.schema.topNodeType.create(null, node).content;
215
+ return blockMarkdown(this.core.getHTMLFromFragment(fragment, node.type.schema), raw);
216
+ }
217
+ reparses(markdown, node, schema) {
218
+ const html = blockHtml(markdown, this.source.links);
219
+ if (html === null) return false;
220
+ const parsed = this.core.createDocument(html, schema);
221
+ const nodes = [];
222
+ parsed.forEach((child) => {
223
+ if (!isEmptyParagraph(child)) nodes.push(child);
224
+ });
225
+ return nodes.length === 1 && JSON.stringify(withoutBlockIds(nodes[0].toJSON())) === JSON.stringify(withoutBlockIds(node.toJSON()));
226
+ }
227
+ mergerFor(id, initial, raw) {
228
+ let merge = this.mergers.get(id);
229
+ if (!merge) {
230
+ merge = createBlockMerger(raw, this.toMarkdown(initial, raw));
231
+ this.mergers.set(id, merge);
232
+ }
233
+ return merge;
234
+ }
235
+ /** The file's Markdown for `doc`: original bytes wherever the reader changed nothing. */
236
+ serialize(doc) {
237
+ const nodes = [];
238
+ doc.forEach((node) => {
239
+ const id = blockIdOf(node);
240
+ const block = id ? this.source.byId.get(id) : void 0;
241
+ const initial = id ? this.initial.get(id) : void 0;
242
+ const raw = node.type.name === "markdownSource" ? String(node.attrs.raw ?? "") : null;
243
+ nodes.push({
244
+ block: initial ? block : void 0,
245
+ unchanged: Boolean(initial?.eq(node)),
246
+ sameType: initial?.type === node.type,
247
+ markdown: () => raw ?? this.toMarkdown(node, block?.raw ?? ""),
248
+ merge: (edited) => initial && block ? this.mergerFor(block.id, initial, block.raw)(edited) : null,
249
+ reparses: (markdown) => this.reparses(markdown, node, node.type.schema)
250
+ });
251
+ });
252
+ return serializeSourceMarkdown(this.source, nodes);
253
+ }
254
+ };
255
+ //#endregion
256
+ export { SOURCE_NODE, SourceMarkdownSession, createSourceMarkdownExtensions };