@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
@@ -0,0 +1,235 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ blockHtml,
4
+ blockMarkdown,
5
+ diffText,
6
+ mergeBlockEdit,
7
+ parseSourceMarkdown,
8
+ serializeSourceMarkdown,
9
+ type SourceBlock,
10
+ type SourceMarkdown,
11
+ type SourceNode,
12
+ } from "./source-markdown";
13
+ import { KITCHEN_SINK } from "./source-markdown.fixtures";
14
+
15
+ /** The serializer's view of a document nobody edited. */
16
+ function untouched(source: SourceMarkdown): SourceNode[] {
17
+ return source.blocks.map((block) => ({
18
+ block,
19
+ unchanged: true,
20
+ sameType: true,
21
+ markdown: () => block.raw,
22
+ merge: () => block.raw,
23
+ reparses: () => true,
24
+ }));
25
+ }
26
+
27
+ /** A node whose Markdown changed to `edited`, merged the way the editor merges it. */
28
+ function editedNode(block: SourceBlock, canonical: string, edited: string): SourceNode {
29
+ return {
30
+ block,
31
+ unchanged: false,
32
+ sameType: true,
33
+ markdown: () => edited,
34
+ merge: (text) => mergeBlockEdit(block.raw, canonical, text),
35
+ reparses: () => true,
36
+ };
37
+ }
38
+
39
+ function addedNode(markdown: string): SourceNode {
40
+ return {
41
+ unchanged: false,
42
+ sameType: false,
43
+ markdown: () => markdown,
44
+ merge: () => null,
45
+ reparses: () => true,
46
+ };
47
+ }
48
+
49
+ describe("parseSourceMarkdown", () => {
50
+ it("splits a file into blocks whose bytes reassemble it exactly", () => {
51
+ const source = parseSourceMarkdown(KITCHEN_SINK);
52
+ const rebuilt = source.prefix + source.blocks.map((block) => block.raw + block.gap).join("");
53
+ expect(rebuilt).toBe(KITCHEN_SINK);
54
+ expect(source.prefix).toBe("\n\n");
55
+ });
56
+
57
+ it("keeps blocks the editor cannot represent read-only, by what they are", () => {
58
+ const kinds = parseSourceMarkdown(KITCHEN_SINK)
59
+ .blocks.filter((block) => !block.editable)
60
+ .map((block) => block.kind);
61
+ expect(kinds).toEqual(["checklist", "table", "html", "image", "html", "definition"]);
62
+ });
63
+
64
+ it("gives the editor a source node for a table, carrying its raw text", () => {
65
+ const table = "| A | B |\n|---|---|\n| 1 | 2 |";
66
+ const source = parseSourceMarkdown(`Intro\n\n${table}\n`);
67
+ expect(source.html).toContain('data-md-kind="table"');
68
+ expect(source.html).toContain('data-md-raw="| A | B |\n|---|---|\n| 1 | 2 |"');
69
+ expect(source.html).toMatch(/^<p data-md-block="m[0-9a-z]+:0">Intro<\/p>/);
70
+ });
71
+
72
+ it("keeps CRLF line endings and a missing final newline", () => {
73
+ for (const file of ["one\r\n\r\n- a\r\n- b\r\n", "no final newline", "\ttab start\n\n-\ttab bullet"]) {
74
+ const source = parseSourceMarkdown(file);
75
+ expect(serializeSourceMarkdown(source, untouched(source))).toBe(file);
76
+ }
77
+ });
78
+
79
+ it("serializes an untouched file byte for byte", () => {
80
+ const source = parseSourceMarkdown(KITCHEN_SINK);
81
+ expect(serializeSourceMarkdown(source, untouched(source))).toBe(KITCHEN_SINK);
82
+ });
83
+
84
+ it("resolves reference links defined elsewhere when it re-reads one block", () => {
85
+ const source = parseSourceMarkdown("See [docs][d].\n\n[d]: https://docs.example\n");
86
+ expect(blockHtml("See [the docs][d].", source.links)).toContain('href="https://docs.example"');
87
+ expect(blockHtml("| a |\n|---|\n| 1 |")).toBeNull();
88
+ });
89
+ });
90
+
91
+ describe("mergeBlockEdit", () => {
92
+ it("keeps escapes the re-serialization would add, outside the edit", () => {
93
+ const raw = "Rename snake_case_name and *keep* it";
94
+ const canonical = "Rename snake\\_case\\_name and *keep* it";
95
+ expect(mergeBlockEdit(raw, canonical, "Rename snake\\_case\\_name and *keep* it now")).toBe(
96
+ "Rename snake_case_name and *keep* it now",
97
+ );
98
+ });
99
+
100
+ it("keeps the source's list markers and indentation around an edited item", () => {
101
+ const raw = "* star bullet one\n* star bullet two\n * nested four-space";
102
+ const canonical = "- star bullet one\n- star bullet two\n - nested four-space";
103
+ const edited = "- star bullet uno\n- star bullet two\n - nested four-space";
104
+ expect(mergeBlockEdit(raw, canonical, edited)).toBe(
105
+ "* star bullet uno\n* star bullet two\n * nested four-space",
106
+ );
107
+ });
108
+
109
+ it("keeps a setext heading's underline and underscore emphasis", () => {
110
+ expect(mergeBlockEdit("Setext heading\n==============", "# Setext heading", "# Setext heading two")).toBe(
111
+ "Setext heading two\n==============",
112
+ );
113
+ expect(mergeBlockEdit("an _emphasised_ word", "an *emphasised* word", "an *emphasised* word, edited")).toBe(
114
+ "an _emphasised_ word, edited",
115
+ );
116
+ });
117
+
118
+ it("keeps a reference link and a backslash hard break the canonical form spells differently", () => {
119
+ expect(
120
+ mergeBlockEdit(
121
+ "A [ref link][ref] here",
122
+ 'A [ref link](https://ref.example "Ref Title") here',
123
+ 'A [ref link](https://ref.example "Ref Title") here and there',
124
+ ),
125
+ ).toBe("A [ref link][ref] here and there");
126
+ expect(mergeBlockEdit("one\\\ntwo", "one \ntwo", "one! \ntwo")).toBe("one!\\\ntwo");
127
+ });
128
+
129
+ it("deletes the source's own spelling of a deleted span", () => {
130
+ expect(mergeBlockEdit("keep snake_case drop", "keep snake\\_case drop", "keep drop")).toBe("keep drop");
131
+ });
132
+
133
+ it("returns the original bytes when the edit was undone", () => {
134
+ expect(mergeBlockEdit("* a", "- a", "- a")).toBe("* a");
135
+ });
136
+ });
137
+
138
+ describe("diffText", () => {
139
+ // Seeded, so a failure reproduces.
140
+ function random(seed: number) {
141
+ let state = seed;
142
+ return () => {
143
+ state = (state * 1103515245 + 12345) % 2147483648;
144
+ return state / 2147483648;
145
+ };
146
+ }
147
+
148
+ it("turns the first text into the second for random edits", () => {
149
+ const next = random(42);
150
+ const alphabet = "ab *_-\n#[]()`\\";
151
+ for (let round = 0; round < 300; round++) {
152
+ const length = Math.floor(next() * 60);
153
+ let a = "";
154
+ for (let i = 0; i < length; i++) a += alphabet[Math.floor(next() * alphabet.length)];
155
+ let b = a;
156
+ const edits = 1 + Math.floor(next() * 4);
157
+ for (let e = 0; e < edits; e++) {
158
+ const at = Math.floor(next() * (b.length + 1));
159
+ const cut = Math.floor(next() * 4);
160
+ const insert = alphabet[Math.floor(next() * alphabet.length)].repeat(Math.floor(next() * 3));
161
+ b = b.slice(0, at) + insert + b.slice(at + cut);
162
+ }
163
+ const ops = diffText(a, b);
164
+ expect(ops).not.toBeNull();
165
+ expect(ops!.filter(([op]) => op !== 1).map(([, text]) => text).join("")).toBe(a);
166
+ expect(ops!.filter(([op]) => op !== -1).map(([, text]) => text).join("")).toBe(b);
167
+ }
168
+ });
169
+
170
+ it("gives up past its cost bound rather than allocating without limit", () => {
171
+ expect(diffText("a".repeat(400), "b".repeat(400), 100)).toBeNull();
172
+ });
173
+ });
174
+
175
+ describe("serializeSourceMarkdown", () => {
176
+ const file = "# Title\n\nFirst paragraph.\n\n* one\n* two\n\n| A |\n|---|\n| 1 |\n";
177
+
178
+ it("changes only the edited span of an edited block", () => {
179
+ const source = parseSourceMarkdown(file);
180
+ const nodes = untouched(source);
181
+ nodes[2] = editedNode(source.blocks[2], "- one\n- two", "- one\n- two\n- three");
182
+ expect(serializeSourceMarkdown(source, nodes)).toBe(
183
+ "# Title\n\nFirst paragraph.\n\n* one\n* two\n- three\n\n| A |\n|---|\n| 1 |\n",
184
+ );
185
+ });
186
+
187
+ it("separates an added block with a blank line and keeps the file's trailer", () => {
188
+ const source = parseSourceMarkdown(file);
189
+ const nodes = untouched(source);
190
+ nodes.splice(2, 0, addedNode("A new paragraph."));
191
+ nodes.push(addedNode("Closing line."));
192
+ expect(serializeSourceMarkdown(source, nodes)).toBe(
193
+ "# Title\n\nFirst paragraph.\n\nA new paragraph.\n\n* one\n* two\n\n| A |\n|---|\n| 1 |\n\nClosing line.\n",
194
+ );
195
+ });
196
+
197
+ it("drops a deleted block and its separator only", () => {
198
+ const source = parseSourceMarkdown(file);
199
+ const nodes = untouched(source).filter((_, i) => i !== 1);
200
+ expect(serializeSourceMarkdown(source, nodes)).toBe("# Title\n\n* one\n* two\n\n| A |\n|---|\n| 1 |\n");
201
+ });
202
+
203
+ it("skips an empty added paragraph, such as the editor's trailing one", () => {
204
+ const source = parseSourceMarkdown(file);
205
+ expect(serializeSourceMarkdown(source, [...untouched(source), addedNode("")])).toBe(file);
206
+ });
207
+
208
+ it("writes added text with the file's CRLF line endings", () => {
209
+ const source = parseSourceMarkdown("one\r\n");
210
+ expect(serializeSourceMarkdown(source, [...untouched(source), addedNode("two\nlines")])).toBe(
211
+ "one\r\n\r\ntwo\r\nlines\r\n",
212
+ );
213
+ });
214
+
215
+ it("falls back to the edited block when the merge does not read back as the edit", () => {
216
+ const source = parseSourceMarkdown("* one\n* two\n");
217
+ const node = editedNode(source.blocks[0], "- one\n- two", "- one\n- two\n- three");
218
+ node.reparses = () => false;
219
+ expect(serializeSourceMarkdown(source, [node])).toBe("- one\n- two\n- three\n");
220
+ });
221
+ });
222
+
223
+ describe("blockMarkdown", () => {
224
+ it("writes lists with one space after the marker, in the source's marker style", () => {
225
+ const html = "<ul><li><p>one</p></li><li><p>two</p></li></ul>";
226
+ expect(blockMarkdown(html)).toBe("- one\n- two");
227
+ expect(blockMarkdown(html, "* x")).toBe("* one\n* two");
228
+ expect(blockMarkdown('<ol start="3"><li><p>a</p></li><li><p>b</p></li></ol>', "3) a")).toBe("3) a\n4) b");
229
+ });
230
+
231
+ it("writes strikethrough and rules as GFM", () => {
232
+ expect(blockMarkdown("<p><s>gone</s></p>")).toBe("~~gone~~");
233
+ expect(blockMarkdown("<hr>")).toBe("---");
234
+ });
235
+ });