Skip to content

Commit 57f6f73

Browse files
Render markdown tables in summaries
Add table parsing, editor schema support, and markdown round-trip tests for generated summary tables.
1 parent 45eda4f commit 57f6f73

3 files changed

Lines changed: 565 additions & 0 deletions

File tree

packages/editor/src/markdown.test.ts

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Node as PMNode } from "prosemirror-model";
12
import { describe, expect, test } from "vitest";
23

34
import {
@@ -8,6 +9,7 @@ import {
89
md2json,
910
parseJsonContent,
1011
} from "./markdown";
12+
import { schema as noteSchema } from "./note/schema";
1113

1214
describe("json2md", () => {
1315
test("renders underline as html tags", () => {
@@ -106,6 +108,255 @@ describe("json2md", () => {
106108
'![alt text](https://example.com/image.png "char-editor-width=42|Example")',
107109
);
108110
});
111+
112+
test("renders table nodes as markdown tables", () => {
113+
const markdown = json2md({
114+
type: "doc",
115+
content: [
116+
{
117+
type: "table",
118+
content: [
119+
{
120+
type: "tableRow",
121+
content: [
122+
{
123+
type: "tableHeader",
124+
content: [
125+
{
126+
type: "paragraph",
127+
content: [{ type: "text", text: "Name" }],
128+
},
129+
],
130+
},
131+
{
132+
type: "tableHeader",
133+
content: [
134+
{
135+
type: "paragraph",
136+
content: [{ type: "text", text: "Role | Notes" }],
137+
},
138+
],
139+
},
140+
],
141+
},
142+
{
143+
type: "tableRow",
144+
content: [
145+
{
146+
type: "tableCell",
147+
content: [
148+
{
149+
type: "paragraph",
150+
content: [{ type: "text", text: "Alasdair" }],
151+
},
152+
],
153+
},
154+
{
155+
type: "tableCell",
156+
content: [
157+
{
158+
type: "paragraph",
159+
content: [{ type: "text", text: "Account Executive" }],
160+
},
161+
],
162+
},
163+
],
164+
},
165+
],
166+
},
167+
],
168+
});
169+
170+
expect(markdown).toBe(
171+
"| Name | Role \\| Notes |\n| --- | --- |\n| Alasdair | Account Executive |",
172+
);
173+
});
174+
175+
test("renders table cell hard breaks as parseable break tags", () => {
176+
const markdown = json2md({
177+
type: "doc",
178+
content: [
179+
{
180+
type: "table",
181+
content: [
182+
{
183+
type: "tableRow",
184+
content: [
185+
{
186+
type: "tableHeader",
187+
content: [
188+
{
189+
type: "paragraph",
190+
content: [{ type: "text", text: "Summary" }],
191+
},
192+
],
193+
},
194+
],
195+
},
196+
{
197+
type: "tableRow",
198+
content: [
199+
{
200+
type: "tableCell",
201+
content: [
202+
{
203+
type: "paragraph",
204+
content: [
205+
{ type: "text", text: "First" },
206+
{ type: "hardBreak" },
207+
{ type: "text", text: "Second" },
208+
],
209+
},
210+
],
211+
},
212+
],
213+
},
214+
],
215+
},
216+
],
217+
});
218+
219+
expect(markdown).toBe("| Summary |\n| --- |\n| First<br>Second |");
220+
});
221+
222+
test("escapes literal table cell break tags", () => {
223+
const markdown = json2md({
224+
type: "doc",
225+
content: [
226+
{
227+
type: "table",
228+
content: [
229+
{
230+
type: "tableRow",
231+
content: [
232+
{
233+
type: "tableHeader",
234+
content: [
235+
{
236+
type: "paragraph",
237+
content: [{ type: "text", text: "Raw" }],
238+
},
239+
],
240+
},
241+
],
242+
},
243+
{
244+
type: "tableRow",
245+
content: [
246+
{
247+
type: "tableCell",
248+
content: [
249+
{
250+
type: "paragraph",
251+
content: [{ type: "text", text: "literal <br>" }],
252+
},
253+
],
254+
},
255+
],
256+
},
257+
],
258+
},
259+
],
260+
});
261+
262+
expect(markdown).toBe("| Raw |\n| --- |\n| literal \\<br> |");
263+
});
264+
265+
test("preserves table cell backslashes across roundtrip", () => {
266+
const json: JSONContent = {
267+
type: "doc",
268+
content: [
269+
{
270+
type: "table",
271+
content: [
272+
{
273+
type: "tableRow",
274+
content: [
275+
{
276+
type: "tableHeader",
277+
content: [
278+
{
279+
type: "paragraph",
280+
content: [{ type: "text", text: "Path" }],
281+
},
282+
],
283+
},
284+
],
285+
},
286+
{
287+
type: "tableRow",
288+
content: [
289+
{
290+
type: "tableCell",
291+
content: [
292+
{
293+
type: "paragraph",
294+
content: [{ type: "text", text: "C:\\Users\\John" }],
295+
},
296+
],
297+
},
298+
],
299+
},
300+
],
301+
},
302+
],
303+
};
304+
305+
const roundtripped = md2json(json2md(json));
306+
const cellText =
307+
roundtripped.content?.[0]?.content?.[1]?.content?.[0]?.content?.[0]
308+
?.content?.[0]?.text;
309+
310+
expect(cellText).toBe("C:\\Users\\John");
311+
});
312+
313+
test("preserves table cell backslashes before pipes across roundtrip", () => {
314+
const json: JSONContent = {
315+
type: "doc",
316+
content: [
317+
{
318+
type: "table",
319+
content: [
320+
{
321+
type: "tableRow",
322+
content: [
323+
{
324+
type: "tableHeader",
325+
content: [
326+
{
327+
type: "paragraph",
328+
content: [{ type: "text", text: "Pattern" }],
329+
},
330+
],
331+
},
332+
],
333+
},
334+
{
335+
type: "tableRow",
336+
content: [
337+
{
338+
type: "tableCell",
339+
content: [
340+
{
341+
type: "paragraph",
342+
content: [{ type: "text", text: "literal \\| marker" }],
343+
},
344+
],
345+
},
346+
],
347+
},
348+
],
349+
},
350+
],
351+
};
352+
353+
const roundtripped = md2json(json2md(json));
354+
const cellText =
355+
roundtripped.content?.[0]?.content?.[1]?.content?.[0]?.content?.[0]
356+
?.content?.[0]?.text;
357+
358+
expect(cellText).toBe("literal \\| marker");
359+
});
109360
});
110361

111362
describe("md2json", () => {
@@ -212,6 +463,48 @@ We appreciate your patience while you wait.`);
212463
expect(json.content![0].attrs?.src).toBe("https://example.com/welcome.png");
213464
expect(json.content![1].type).toBe("paragraph");
214465
});
466+
467+
test("converts markdown tables to editor-compatible table JSON", () => {
468+
const json = md2json(`| Name | Company | Role / Notes |
469+
| --- | --- | --- |
470+
| Alasdair | Cloudflare | Account Executive |
471+
| Rick | Cloudflare | Solutions Engineer |`);
472+
473+
const table = json.content?.[0];
474+
expect(table?.type).toBe("table");
475+
expect(table?.content?.[0]?.type).toBe("tableRow");
476+
expect(table?.content?.[0]?.content?.[0]?.type).toBe("tableHeader");
477+
expect(
478+
table?.content?.[0]?.content?.[0]?.content?.[0]?.content?.[0]?.text,
479+
).toBe("Name");
480+
expect(table?.content?.[1]?.content?.[0]?.type).toBe("tableCell");
481+
expect(() => PMNode.fromJSON(noteSchema, json)).not.toThrow();
482+
});
483+
484+
test("converts table cell break tags to hard breaks", () => {
485+
const json = md2json(`| Summary |
486+
| --- |
487+
| First<br>Second |`);
488+
489+
const cellContent =
490+
json.content?.[0]?.content?.[1]?.content?.[0]?.content?.[0]?.content;
491+
expect(cellContent).toEqual([
492+
{ type: "text", text: "First" },
493+
{ type: "hardBreak" },
494+
{ type: "text", text: "Second" },
495+
]);
496+
expect(() => PMNode.fromJSON(noteSchema, json)).not.toThrow();
497+
});
498+
499+
test("keeps escaped table cell break tags as text", () => {
500+
const json = md2json(`| Raw |
501+
| --- |
502+
| literal \\<br> |`);
503+
504+
const cellContent =
505+
json.content?.[0]?.content?.[1]?.content?.[0]?.content?.[0]?.content;
506+
expect(cellContent).toEqual([{ type: "text", text: "literal <br>" }]);
507+
});
215508
});
216509

217510
describe("roundtrip", () => {

0 commit comments

Comments
 (0)