@kernhq/module-quire 0.13.1 → 0.15.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 (123) hide show
  1. package/dist/contract/models.d.ts +394 -0
  2. package/dist/contract/models.d.ts.map +1 -1
  3. package/dist/contract/models.js +284 -0
  4. package/dist/contract/models.js.map +1 -1
  5. package/dist/contract/permissions.d.ts +14 -0
  6. package/dist/contract/permissions.d.ts.map +1 -1
  7. package/dist/contract/permissions.js +114 -0
  8. package/dist/contract/permissions.js.map +1 -1
  9. package/dist/contract/properties.d.ts +7 -7
  10. package/dist/contract/router.d.ts +1124 -96
  11. package/dist/contract/router.d.ts.map +1 -1
  12. package/dist/contract/router.js +343 -1
  13. package/dist/contract/router.js.map +1 -1
  14. package/dist/server/_impl.d.ts +983 -1168
  15. package/dist/server/_impl.d.ts.map +1 -1
  16. package/dist/server/_impl.js +308 -0
  17. package/dist/server/_impl.js.map +1 -1
  18. package/dist/server/export/html.d.ts +67 -0
  19. package/dist/server/export/html.d.ts.map +1 -0
  20. package/dist/server/export/html.js +206 -0
  21. package/dist/server/export/html.js.map +1 -0
  22. package/dist/server/export/markdown.d.ts +51 -0
  23. package/dist/server/export/markdown.d.ts.map +1 -0
  24. package/dist/server/export/markdown.js +351 -0
  25. package/dist/server/export/markdown.js.map +1 -0
  26. package/dist/server/export/pdf.d.ts +20 -0
  27. package/dist/server/export/pdf.d.ts.map +1 -0
  28. package/dist/server/export/pdf.js +91 -0
  29. package/dist/server/export/pdf.js.map +1 -0
  30. package/dist/server/export/zip.d.ts +31 -0
  31. package/dist/server/export/zip.d.ts.map +1 -0
  32. package/dist/server/export/zip.js +158 -0
  33. package/dist/server/export/zip.js.map +1 -0
  34. package/dist/server/import/csv.d.ts +77 -0
  35. package/dist/server/import/csv.d.ts.map +1 -0
  36. package/dist/server/import/csv.js +263 -0
  37. package/dist/server/import/csv.js.map +1 -0
  38. package/dist/server/import/html.d.ts +52 -0
  39. package/dist/server/import/html.d.ts.map +1 -0
  40. package/dist/server/import/html.js +472 -0
  41. package/dist/server/import/html.js.map +1 -0
  42. package/dist/server/import/markdown.d.ts +63 -0
  43. package/dist/server/import/markdown.d.ts.map +1 -0
  44. package/dist/server/import/markdown.js +692 -0
  45. package/dist/server/import/markdown.js.map +1 -0
  46. package/dist/server/import/plan.d.ts +70 -0
  47. package/dist/server/import/plan.d.ts.map +1 -0
  48. package/dist/server/import/plan.js +761 -0
  49. package/dist/server/import/plan.js.map +1 -0
  50. package/dist/server/import/ydoc.d.ts +35 -0
  51. package/dist/server/import/ydoc.d.ts.map +1 -0
  52. package/dist/server/import/ydoc.js +91 -0
  53. package/dist/server/import/ydoc.js.map +1 -0
  54. package/dist/server/import/zip.d.ts +63 -0
  55. package/dist/server/import/zip.d.ts.map +1 -0
  56. package/dist/server/import/zip.js +308 -0
  57. package/dist/server/import/zip.js.map +1 -0
  58. package/dist/server/index.d.ts.map +1 -1
  59. package/dist/server/index.js +3 -1
  60. package/dist/server/index.js.map +1 -1
  61. package/dist/server/render.d.ts +158 -0
  62. package/dist/server/render.d.ts.map +1 -1
  63. package/dist/server/render.js +240 -0
  64. package/dist/server/render.js.map +1 -1
  65. package/dist/server/schema.d.ts +704 -1
  66. package/dist/server/schema.d.ts.map +1 -1
  67. package/dist/server/schema.js +254 -1
  68. package/dist/server/schema.js.map +1 -1
  69. package/dist/server/services/databases.d.ts +10 -10
  70. package/dist/server/services/export.d.ts +176 -0
  71. package/dist/server/services/export.d.ts.map +1 -0
  72. package/dist/server/services/export.js +822 -0
  73. package/dist/server/services/export.js.map +1 -0
  74. package/dist/server/services/import.d.ts +109 -0
  75. package/dist/server/services/import.d.ts.map +1 -0
  76. package/dist/server/services/import.js +570 -0
  77. package/dist/server/services/import.js.map +1 -0
  78. package/dist/server/services/index.d.ts +32 -1
  79. package/dist/server/services/index.d.ts.map +1 -1
  80. package/dist/server/services/index.js +87 -4
  81. package/dist/server/services/index.js.map +1 -1
  82. package/dist/server/services/macros.d.ts +83 -0
  83. package/dist/server/services/macros.d.ts.map +1 -0
  84. package/dist/server/services/macros.js +488 -0
  85. package/dist/server/services/macros.js.map +1 -0
  86. package/dist/server/services/publications.d.ts +2 -1
  87. package/dist/server/services/publications.d.ts.map +1 -1
  88. package/dist/server/services/publications.js +43 -4
  89. package/dist/server/services/publications.js.map +1 -1
  90. package/dist/server/services/templates.d.ts +135 -0
  91. package/dist/server/services/templates.d.ts.map +1 -0
  92. package/dist/server/services/templates.js +897 -0
  93. package/dist/server/services/templates.js.map +1 -0
  94. package/dist/server/services/versions.d.ts +13 -1
  95. package/dist/server/services/versions.d.ts.map +1 -1
  96. package/dist/server/services/versions.js +3 -1
  97. package/dist/server/services/versions.js.map +1 -1
  98. package/migrations/0010_transfers.sql +154 -0
  99. package/migrations/0011_templates.sql +157 -0
  100. package/migrations/meta/_journal.json +14 -0
  101. package/package.json +5 -5
  102. package/src/client/components/ExportDialog.svelte +685 -0
  103. package/src/client/components/ImportDialog.svelte +702 -0
  104. package/src/client/components/ImportReport.svelte +310 -0
  105. package/src/client/components/NewSpaceDialog.svelte +77 -8
  106. package/src/client/components/PageEditor.svelte +80 -0
  107. package/src/client/components/PagePicker.svelte +264 -0
  108. package/src/client/components/SaveAsTemplateDialog.svelte +502 -0
  109. package/src/client/components/SidebarSpaces.svelte +112 -1
  110. package/src/client/components/TemplatePicker.svelte +437 -0
  111. package/src/client/i18n.ts +941 -0
  112. package/src/client/index.ts +51 -0
  113. package/src/client/mock.ts +592 -0
  114. package/src/client/module.ts +36 -0
  115. package/src/client/pages/PageView.svelte +86 -0
  116. package/src/client/pages/SpacePage.svelte +20 -4
  117. package/src/client/pages/TransfersPage.svelte +570 -0
  118. package/src/client/permissions.ts +11 -0
  119. package/src/client/query.ts +40 -0
  120. package/src/client/transfers.ts +142 -0
  121. package/src/contract/models.ts +343 -0
  122. package/src/contract/permissions.ts +117 -0
  123. package/src/contract/router.ts +375 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A page as Markdown, written from the page document rather than from HTML.
3
+ *
4
+ * Two writers, one source. `renderPageDoc` next door draws the same document as HTML; this one draws
5
+ * it as Markdown, and both read the stored `PageDoc`. Converting the HTML instead would have been
6
+ * less code and would have thrown away everything a heading, a task and a table know about
7
+ * themselves — an `<h2>` is a heading, but a `<ul class="kern-tasks">` full of disabled checkboxes is
8
+ * not a task list to anything that reads it back.
9
+ *
10
+ * **Round-tripping is the requirement, not prettiness.** A heading, a list, a table, a code block, a
11
+ * callout and a task list have to come back as themselves when the file is imported again, so every
12
+ * one of them is written in the dialect an importer can recognise without guessing:
13
+ *
14
+ * - tables and task lists are GitHub-flavoured Markdown, because that is what every reader
15
+ * implements and what an import will parse;
16
+ * - a callout is a blockquote with a GitHub alert marker — `> [!WARNING]` — carrying Quire's own
17
+ * tone name, so it re-reads as the same callout here and degrades to an ordinary blockquote
18
+ * anywhere else. A `<div data-callout>` would round-trip too and would be a raw HTML block in
19
+ * every other reader;
20
+ * - underline and highlight have no Markdown at all and are written as `<u>` and `<mark>`, which
21
+ * is inline HTML a Markdown parser passes through rather than a syntax it will mangle.
22
+ *
23
+ * `PAGE_DOC_NODES` is the list this has to cover, the same list `render.ts` is held to, and
24
+ * `export.int.test.ts` compares this file's table against it in both directions. A node with no
25
+ * writer here does not degrade quietly — it would lose somebody's table.
26
+ */
27
+ import type { PageDoc } from '@kernhq/ui/editor/page-doc';
28
+ export interface MarkdownOptions {
29
+ /**
30
+ * Where the picture with this id sits *relative to the file being written* — `media/diagram.png`.
31
+ *
32
+ * Never a signed storage URL. A presigned GET is the object's key, so it carries the tenant's
33
+ * workspace uuid and the file's uuid, and it stops working an hour after it is minted: writing one
34
+ * into a file somebody keeps is both a leak and a broken picture. A picture with no answer here is
35
+ * dropped, exactly as the HTML renderer drops it.
36
+ */
37
+ fileSrc?: (fileId: string) => string | null;
38
+ /** Where another exported page sits relative to this one. Without one, a mention stays as text. */
39
+ pageHref?: (pageId: string) => string | null;
40
+ }
41
+ /** Every node type this file can write, for the test that compares it against `PAGE_DOC_NODES`. */
42
+ export declare const MARKDOWN_NODES: readonly string[];
43
+ /**
44
+ * A stored page as Markdown. An empty or unreadable document is an empty string, never a throw.
45
+ *
46
+ * The title is not written here. A page's title is a column, not a node in its document, so whether
47
+ * it becomes an `# H1` at the top is a decision about the file — and the file writer next door makes
48
+ * it, once, for both formats.
49
+ */
50
+ export declare function pageDocToMarkdown(doc: PageDoc | null | undefined, options?: MarkdownOptions): string;
51
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../src/server/export/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,OAAO,EAA4B,MAAM,4BAA4B,CAAA;AAEnF,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;IAC3C,mGAAmG;IACnG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;CAC7C;AAgXD,mGAAmG;AACnG,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAE3C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAKxG"}
@@ -0,0 +1,351 @@
1
+ /**
2
+ * What a Markdown reader would otherwise take as syntax.
3
+ *
4
+ * Deliberately not every character CommonMark lists. Escaping `.`, `+`, `(` and `)` everywhere turns
5
+ * an ordinary sentence into a thicket of backslashes, and they are only meaningful at the start of a
6
+ * line or inside a link — both of which are handled where they arise. What is escaped here is what
7
+ * changes the meaning of a sentence wherever it appears.
8
+ */
9
+ const escapeInline = (text) => text.replace(/([\\`*_[\]<>|~])/g, '\\$1');
10
+ /**
11
+ * A line that would start a block if it were left alone — a paragraph beginning `- ` or `# `.
12
+ *
13
+ * The ordered-list case is separate because the escape goes in a different place. `\1. ` is not an
14
+ * escape at all: a backslash before anything that is not punctuation is a literal backslash, so it
15
+ * would print one and still start a list. `1\. ` is the one that works.
16
+ */
17
+ const escapeLineStart = (line) => line
18
+ .replace(/^(\s*)(>|#{1,6}(?=\s)|[-*+](?=\s))/, '$1\\$2')
19
+ .replace(/^(\s*)(\d{1,9})([.)])(?=\s)/, '$1$2\\$3');
20
+ /** Every character in the node, ignoring marks. What a fence and a `<pre>` want. */
21
+ function textOf(node) {
22
+ if (typeof node.text === 'string')
23
+ return node.text;
24
+ return (node.content ?? []).map(textOf).join('');
25
+ }
26
+ /**
27
+ * The marks, applied so the result parses.
28
+ *
29
+ * Order matters and is not alphabetical. `code` goes on first because a code span's content is
30
+ * literal — `**bold**` inside backticks is four asterisks and a word — and `link` goes on last
31
+ * because a link's label may hold everything else and nothing may hold a link.
32
+ */
33
+ function applyMarks(text, marks) {
34
+ const has = (type) => (marks ?? []).some((m) => m?.type === type);
35
+ let out = has('code') ? codeSpan(text) : escapeInline(text);
36
+ if (has('highlight'))
37
+ out = `<mark>${out}</mark>`;
38
+ if (has('strike'))
39
+ out = `~~${out}~~`;
40
+ if (has('underline'))
41
+ out = `<u>${out}</u>`;
42
+ if (has('italic'))
43
+ out = `*${out}*`;
44
+ if (has('bold'))
45
+ out = `**${out}**`;
46
+ const link = (marks ?? []).find((m) => m?.type === 'link');
47
+ if (link) {
48
+ const href = typeof link.attrs?.href === 'string' ? link.attrs.href : '';
49
+ if (href)
50
+ out = `[${out}](${encodeLinkTarget(href)})`;
51
+ }
52
+ return out;
53
+ }
54
+ /** A code span whose fence is longer than any run of backticks inside it, so it closes where it should. */
55
+ function codeSpan(text) {
56
+ const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
57
+ const fence = '`'.repeat(longest + 1);
58
+ const pad = text.startsWith('`') || text.endsWith('`') ? ' ' : '';
59
+ return `${fence}${pad}${text}${pad}${fence}`;
60
+ }
61
+ /** Parentheses and whitespace are what break an inline link target; angle brackets fix both. */
62
+ const encodeLinkTarget = (href) => /[\s()<>]/.test(href) ? `<${href.replaceAll('>', '%3E')}>` : href;
63
+ function inline(nodes, options) {
64
+ return (nodes ?? [])
65
+ .map((node) => {
66
+ if (typeof node.text === 'string')
67
+ return applyMarks(node.text, node.marks);
68
+ const writer = INLINE_WRITERS[node.type ?? ''];
69
+ if (writer)
70
+ return writer(node, options);
71
+ // An inline node with no writer keeps its text rather than vanishing mid-sentence.
72
+ return escapeInline(textOf(node));
73
+ })
74
+ .join('');
75
+ }
76
+ const INLINE_WRITERS = {
77
+ // Two trailing spaces is the line break every Markdown reader implements; a backslash is CommonMark
78
+ // only, and an importer that is not CommonMark would keep the backslash as a character.
79
+ hardBreak: () => ' \n',
80
+ text: (node) => escapeInline(node.text ?? ''),
81
+ /**
82
+ * A person, as `@Ada` — **and this one does not come back.**
83
+ *
84
+ * Stated here because it is the one construct in the writer that is knowingly lossy, and a reader
85
+ * of this table would otherwise assume the pairing with `import/markdown.ts` is total. A page
86
+ * mention resolves against a path in the same archive, so the reader can rebuild it; a person
87
+ * resolves against a *user id*, which is a fact about the instance the export came from. The two
88
+ * spellings that would carry it are both worse than the loss: putting the id in the file makes an
89
+ * export identify people, and importing it would mint a mention of a stranger's id in a workspace
90
+ * that never had that person. So this writes what a mention reads as — the name — and it re-imports
91
+ * as prose. There is no report row for it either, because by then it is a run of text and nothing
92
+ * can tell it from one somebody typed.
93
+ */
94
+ mention: (node) => `@${escapeInline(String(node.attrs?.label ?? ''))}`,
95
+ pageMention: (node, options) => {
96
+ const label = escapeInline(String(node.attrs?.label ?? '') || 'Untitled');
97
+ const id = typeof node.attrs?.id === 'string' ? node.attrs.id : '';
98
+ const href = id ? (options.pageHref?.(id) ?? null) : null;
99
+ // A page that is not in this export cannot be linked to, and a dead link is worse than a name.
100
+ return href ? `[${label}](${encodeLinkTarget(href)})` : label;
101
+ },
102
+ image: (node, options) => {
103
+ const fileId = typeof node.attrs?.fileId === 'string' ? node.attrs.fileId : null;
104
+ const resolved = fileId ? (options.fileSrc?.(fileId) ?? null) : null;
105
+ const src = resolved ?? (typeof node.attrs?.src === 'string' ? node.attrs.src : null);
106
+ if (!src)
107
+ return '';
108
+ const alt = escapeInline(typeof node.attrs?.alt === 'string' ? node.attrs.alt : '');
109
+ const title = typeof node.attrs?.title === 'string' && node.attrs.title ? node.attrs.title : null;
110
+ return `![${alt}](${encodeLinkTarget(src)}${title ? ` "${title.replaceAll('"', '\\"')}"` : ''})`;
111
+ },
112
+ };
113
+ /**
114
+ * Prefix every line, including the blank ones — which is what makes a nested blockquote hold.
115
+ *
116
+ * A blank line gets the prefix trimmed (`>` rather than `> `) and a line with content does not, so
117
+ * the two trailing spaces that mean "line break" survive. Trimming every line would delete them and
118
+ * silently join two lines of an address.
119
+ */
120
+ const prefixLines = (text, first, rest = first) => text
121
+ .split('\n')
122
+ .map((line, i) => line.length === 0 ? (i === 0 ? first : rest).trimEnd() : `${i === 0 ? first : rest}${line}`)
123
+ .join('\n');
124
+ /** Blocks, separated by the blank line that keeps them separate blocks. */
125
+ function blocks(nodes, options) {
126
+ return (nodes ?? [])
127
+ .map((node) => block(node, options))
128
+ .filter((text) => text.length > 0)
129
+ .join('\n\n');
130
+ }
131
+ function block(node, options) {
132
+ const writer = BLOCK_WRITERS[node.type ?? ''];
133
+ if (writer)
134
+ return writer(node, options);
135
+ // Unreachable while the test below holds, and correct if a newer image writes a node this one has
136
+ // never heard of: keep the children rather than losing a paragraph to an unknown wrapper.
137
+ return blocks(node.content, options);
138
+ }
139
+ /**
140
+ * A list, whose marker depends on the list and whose continuation depends on the marker.
141
+ *
142
+ * Every line after the first is indented by the marker's own width, or the reader ends the item and
143
+ * starts a paragraph — which is how a two-paragraph bullet becomes a bullet and an orphan.
144
+ */
145
+ function list(node, options, markerFor) {
146
+ return (node.content ?? [])
147
+ .map((item, index) => {
148
+ const marker = markerFor(index);
149
+ const body = blocks(item.content, options);
150
+ // An empty bullet is still a bullet somebody typed, so the marker survives on its own.
151
+ return body ? prefixLines(body, marker, ' '.repeat(marker.length)) : marker.trimEnd();
152
+ })
153
+ .join('\n');
154
+ }
155
+ /** `info` | `note` | `success` | `warning` | `danger`, upper-cased into a GitHub alert marker. */
156
+ const CALLOUT_TONES = new Set(['info', 'note', 'success', 'warning', 'danger']);
157
+ /**
158
+ * Escape the pipes a cell still has, and only those.
159
+ *
160
+ * `escapeInline` already escaped every pipe in ordinary prose, so a blind `replaceAll` would double
161
+ * them — `Pipe \| here` becomes `Pipe \\| here`, which renders as a backslash and *then* ends the
162
+ * cell. What is left unescaped is a pipe inside a code span, where escaping is deliberately not
163
+ * applied — and GFM says a table cell escapes those too, code span or not. So this walks the string,
164
+ * steps over anything already escaped, and escapes what remains.
165
+ */
166
+ function escapePipes(text) {
167
+ let out = '';
168
+ for (let i = 0; i < text.length; i++) {
169
+ const ch = text[i];
170
+ if (ch === '\\' && i + 1 < text.length) {
171
+ out += ch + text[i + 1];
172
+ i++;
173
+ continue;
174
+ }
175
+ out += ch === '|' ? '\\|' : ch;
176
+ }
177
+ return out;
178
+ }
179
+ /**
180
+ * One cell of a GFM table.
181
+ *
182
+ * A cell is inline only — the format has no way to hold a second paragraph — so a paragraph break
183
+ * becomes `<br><br>`. Losing the block structure inside a cell is the format's limit, and saying so
184
+ * here is better than a table that stops being a table at the first newline.
185
+ *
186
+ * The whitespace before each newline goes with it, and that is not tidiness. A hard break is written
187
+ * ` \n` — two spaces are what makes it a break in prose — so leaving them in front of the `<br>`
188
+ * hands the importer a cell whose text ends in two spaces it never had. They are invisible in every
189
+ * rendering and they *accumulate*: three export-import cycles turned `a` into `a` and six spaces.
190
+ */
191
+ const tableCell = (cell, options) => escapePipes((blocks(cell.content, options) || '')
192
+ .replace(/[ \t]*\n{2,}/g, '<br><br>')
193
+ .replace(/[ \t]*\n/g, '<br>')
194
+ .trim());
195
+ function table(node, options) {
196
+ const rows = (node.content ?? []).filter((row) => (row.content ?? []).length > 0);
197
+ if (rows.length === 0)
198
+ return '';
199
+ const width = Math.max(...rows.map((row) => (row.content ?? []).length));
200
+ const cellsOf = (row) => {
201
+ const cells = (row.content ?? []).map((cell) => tableCell(cell, options));
202
+ while (cells.length < width)
203
+ cells.push('');
204
+ return cells;
205
+ };
206
+ /*
207
+ * GFM has no table without a header row, so a table whose first row is ordinary cells still
208
+ * contributes one — its own first row. Emitting an empty header instead would put a blank band at
209
+ * the top of every table Quire exports, and shift every row's meaning by one on the way back in.
210
+ */
211
+ const [head, ...body] = rows;
212
+ const line = (cells) => `| ${cells.join(' | ')} |`;
213
+ return [
214
+ line(cellsOf(head)),
215
+ line(Array.from({ length: width }, () => '---')),
216
+ ...body.map((row) => line(cellsOf(row))),
217
+ ].join('\n');
218
+ }
219
+ const BLOCK_WRITERS = {
220
+ paragraph: (node, options) => escapeLineStart(inline(node.content, options)),
221
+ heading: (node, options) => {
222
+ const raw = Number(node.attrs?.level);
223
+ const level = Number.isInteger(raw) && raw >= 1 && raw <= 6 ? raw : 1;
224
+ return `${'#'.repeat(level)} ${inline(node.content, options)}`.trimEnd();
225
+ },
226
+ bulletList: (node, options) => list(node, options, () => '- '),
227
+ orderedList: (node, options) => {
228
+ const raw = Number(node.attrs?.start);
229
+ const start = Number.isInteger(raw) && raw > 0 ? raw : 1;
230
+ return list(node, options, (index) => `${start + index}. `);
231
+ },
232
+ listItem: (node, options) => blocks(node.content, options),
233
+ taskList: (node, options) => (node.content ?? [])
234
+ .map((item) => {
235
+ const marker = `- [${item.attrs?.checked === true ? 'x' : ' '}] `;
236
+ const body = blocks(item.content, options);
237
+ return body ? prefixLines(body, marker, ' '.repeat(marker.length)) : marker.trimEnd();
238
+ })
239
+ .join('\n'),
240
+ taskItem: (node, options) => blocks(node.content, options),
241
+ blockquote: (node, options) => prefixLines(blocks(node.content, options), '> '),
242
+ /**
243
+ * A GitHub alert: a blockquote whose first line names the tone.
244
+ *
245
+ * The tone is Quire's own word rather than GitHub's nearest equivalent, because the round trip
246
+ * matters more than the rendering on one website. `[!SUCCESS]` is not one of GitHub's five, so it
247
+ * renders there as a plain blockquote with a line of text at the top — which is a smaller loss
248
+ * than mapping `success` onto `TIP` and importing it back as the wrong colour.
249
+ */
250
+ callout: (node, options) => {
251
+ const tone = String(node.attrs?.tone ?? 'info');
252
+ const marker = CALLOUT_TONES.has(tone) ? tone : 'info';
253
+ return prefixLines(`[!${marker.toUpperCase()}]\n${blocks(node.content, options)}`, '> ');
254
+ },
255
+ codeBlock: (node) => {
256
+ const language = typeof node.attrs?.language === 'string' && /^[a-z0-9#+.-]{1,24}$/i.test(node.attrs.language)
257
+ ? node.attrs.language
258
+ : '';
259
+ const body = textOf(node);
260
+ // Longer than any run of backticks in the body, or a snippet about Markdown closes its own fence.
261
+ const longest = Math.max(2, ...[...body.matchAll(/^`{3,}/gm)].map((m) => m[0].length));
262
+ const fence = '`'.repeat(longest + 1);
263
+ return `${fence}${language}\n${body}\n${fence}`;
264
+ },
265
+ horizontalRule: () => '---',
266
+ table,
267
+ // A row or a cell reached on its own is inside a table that has already written it; reaching one
268
+ // here would mean a malformed document, and its text is better kept than dropped.
269
+ tableRow: (node, options) => blocks(node.content, options),
270
+ tableCell: (node, options) => blocks(node.content, options),
271
+ tableHeader: (node, options) => blocks(node.content, options),
272
+ /**
273
+ * A toggle, as the HTML block Markdown has no syntax for.
274
+ *
275
+ * The blank lines around the body are load-bearing: without them a Markdown reader treats
276
+ * everything between the tags as raw HTML and stops parsing the prose inside, so a bulleted list
277
+ * inside a toggle arrives as literal hyphens.
278
+ */
279
+ details: (node, options) => {
280
+ const summary = (node.content ?? []).find((child) => child.type === 'detailsSummary');
281
+ const rest = (node.content ?? []).filter((child) => child.type !== 'detailsSummary');
282
+ const title = summary ? inline(summary.content, options) : '';
283
+ return `<details>\n<summary>${title}</summary>\n\n${blocks(rest, options)}\n\n</details>`;
284
+ },
285
+ detailsSummary: (node, options) => inline(node.content, options),
286
+ detailsContent: (node, options) => blocks(node.content, options),
287
+ /*
288
+ * The macros.
289
+ *
290
+ * **A file is not a reader, so no macro that reads other pages writes anything here.** This
291
+ * writer is handed one document and nothing else — no principal, no publication, no resolver —
292
+ * which is exactly the state in which the HTML renderer next door draws an empty frame. Writing
293
+ * a placeholder sentence would be worse than the blank it replaces: an export is a file somebody
294
+ * keeps, and a line saying "children of this page" that will never fill in is a promise the file
295
+ * cannot keep. An exporter that grows an audience can pass one to `renderPageDoc` and write the
296
+ * result; until then the honest output is nothing.
297
+ *
298
+ * They are still listed rather than left to the unknown-node fallback, because that fallback
299
+ * writes a node's children — and `excerpt` has children that must be written, while the five
300
+ * atoms have none. Naming all eight is what `export.int.test.ts` holds this table to.
301
+ */
302
+ pageChildren: () => '',
303
+ excerptInclude: () => '',
304
+ includePage: () => '',
305
+ recentlyUpdated: () => '',
306
+ contributors: () => '',
307
+ /*
308
+ * An excerpt is this page's own prose, so it is written as itself. The marker is lost, which is
309
+ * the format's limit rather than a decision: Markdown has no way to say "this paragraph is the
310
+ * quotable one", and an HTML wrapper around it would be a raw block in every other reader.
311
+ */
312
+ excerpt: (node, options) => blocks(node.content, options),
313
+ /*
314
+ * An expand is a `<details>` like the toggle above it, and for the same reason — Markdown has no
315
+ * syntax for one. `open` is carried, because the writer stored that decision and a file that
316
+ * silently closes every section loses it.
317
+ */
318
+ expand: (node, options) => {
319
+ const summary = (node.content ?? []).find((child) => child.type === 'detailsSummary');
320
+ const rest = (node.content ?? []).filter((child) => child.type !== 'detailsSummary');
321
+ const title = summary ? inline(summary.content, options) : '';
322
+ const open = node.attrs?.open === true || node.attrs?.open === 'true' ? ' open' : '';
323
+ return `<details${open}>\n<summary>${title}</summary>\n\n${blocks(rest, options)}\n\n</details>`;
324
+ },
325
+ // Blocks in name only: all three are inline nodes that can also stand alone as a whole block.
326
+ image: (node, options) => INLINE_WRITERS.image(node, options),
327
+ hardBreak: () => '',
328
+ mention: (node, options) => INLINE_WRITERS.mention(node, options),
329
+ pageMention: (node, options) => INLINE_WRITERS.pageMention(node, options),
330
+ statusLozenge: (node, options) => INLINE_WRITERS.statusLozenge(node, options),
331
+ text: (node) => escapeInline(node.text ?? ''),
332
+ };
333
+ /** Every node type this file can write, for the test that compares it against `PAGE_DOC_NODES`. */
334
+ export const MARKDOWN_NODES = [
335
+ ...new Set(['doc', ...Object.keys(BLOCK_WRITERS), ...Object.keys(INLINE_WRITERS)]),
336
+ ];
337
+ /**
338
+ * A stored page as Markdown. An empty or unreadable document is an empty string, never a throw.
339
+ *
340
+ * The title is not written here. A page's title is a column, not a node in its document, so whether
341
+ * it becomes an `# H1` at the top is a decision about the file — and the file writer next door makes
342
+ * it, once, for both formats.
343
+ */
344
+ export function pageDocToMarkdown(doc, options = {}) {
345
+ if (!doc || !Array.isArray(doc.content))
346
+ return '';
347
+ return `${blocks(doc.content, options)
348
+ .replace(/\n{3,}/g, '\n\n')
349
+ .trim()}\n`;
350
+ }
351
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../../src/server/export/markdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /** Where the compose stack puts it. `dev/compose.yml` publishes 3500 on the host. */
2
+ export declare const DEFAULT_GOTENBERG_URL = "http://gotenberg:3000";
3
+ export declare const gotenbergUrl: () => string;
4
+ export interface PdfOptions {
5
+ /** overrides `GOTENBERG_URL`; the export job does not pass one, the test does */
6
+ url?: string;
7
+ timeoutMs?: number;
8
+ /** what Chromium prints in the footer's page numbers, and nothing else */
9
+ title?: string;
10
+ }
11
+ /**
12
+ * A complete PDF, or a throw.
13
+ *
14
+ * The HTML has to be self-contained. Gotenberg's Chromium fetches whatever the document references,
15
+ * from inside its own container — so a presigned storage URL in an `<img src>` is both a network
16
+ * dependency this has no business having and a workspace uuid handed to a process that did not need
17
+ * it. Pictures reach here as `data:` URIs, put there by the caller.
18
+ */
19
+ export declare function htmlToPdf(html: string, options?: PdfOptions): Promise<Buffer>;
20
+ //# sourceMappingURL=pdf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.d.ts","sourceRoot":"","sources":["../../../src/server/export/pdf.ts"],"names":[],"mappings":"AA2BA,qFAAqF;AACrF,eAAO,MAAM,qBAAqB,0BAA0B,CAAA;AAE5D,eAAO,MAAM,YAAY,QAAO,MAAoE,CAAA;AAQpG,MAAM,WAAW,UAAU;IACzB,iFAAiF;IACjF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA2DvF"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * HTML to PDF, through Gotenberg.
3
+ *
4
+ * Gotenberg is a Chromium in a container, and it is already in the stack: `selfhost/docker-compose.yml`
5
+ * starts it under `--profile preview`, `dev/compose.yml` publishes it on 3500. Nothing in Kern had
6
+ * ever called it, so this is the first caller and the first thing that has to answer the question
7
+ * every optional dependency raises — **what happens when it is not there.**
8
+ *
9
+ * The answer is one sentence with three parts, and every one of them is load-bearing:
10
+ *
11
+ * - it never produces a half file. The bytes are read in full, in memory, before anything is
12
+ * written anywhere; a refusal, a timeout or a closed port throws before the caller has an
13
+ * artefact to store, so an export job goes to `failed` with `file_id` still null rather than to
14
+ * `done` with a truncated PDF a reader would find out about by opening it;
15
+ * - the failure names the URL it tried and the variable that changes it, because the person who
16
+ * reads it is an operator looking at a stack, not a developer looking at a stack trace. "PDF
17
+ * export could not reach Gotenberg at http://gotenberg:3000" is actionable; `ECONNREFUSED` is
18
+ * not;
19
+ * - it is bounded. Chromium rendering a five-hundred-page handbook can take minutes or can hang,
20
+ * and a job that hangs holds a worker for ever. The request carries a timeout and gives up.
21
+ *
22
+ * The default points at the compose service name rather than at nothing, so an instance that has the
23
+ * `preview` profile on needs no configuration at all and one that does not gets a refusal naming
24
+ * `GOTENBERG_URL`.
25
+ */
26
+ import { KernError } from '@kernhq/kernel';
27
+ /** Where the compose stack puts it. `dev/compose.yml` publishes 3500 on the host. */
28
+ export const DEFAULT_GOTENBERG_URL = 'http://gotenberg:3000';
29
+ export const gotenbergUrl = () => process.env.GOTENBERG_URL?.trim() || DEFAULT_GOTENBERG_URL;
30
+ /** Long enough for a big handbook, short enough that a wedged Chromium is not a wedged worker. */
31
+ const DEFAULT_TIMEOUT_MS = 120_000;
32
+ /** A refusal's body is a diagnostic, not a document: enough to act on, never a page of HTML in a log. */
33
+ const REASON_LIMIT = 400;
34
+ /**
35
+ * A complete PDF, or a throw.
36
+ *
37
+ * The HTML has to be self-contained. Gotenberg's Chromium fetches whatever the document references,
38
+ * from inside its own container — so a presigned storage URL in an `<img src>` is both a network
39
+ * dependency this has no business having and a workspace uuid handed to a process that did not need
40
+ * it. Pictures reach here as `data:` URIs, put there by the caller.
41
+ */
42
+ export async function htmlToPdf(html, options = {}) {
43
+ const base = (options.url ?? gotenbergUrl()).replace(/\/+$/, '');
44
+ const endpoint = `${base}/forms/chromium/convert/html`;
45
+ const form = new FormData();
46
+ // The part *must* be named `index.html`; Gotenberg renders that file and treats the rest as assets.
47
+ form.append('files', new Blob([html], { type: 'text/html' }), 'index.html');
48
+ form.append('paperWidth', '8.27');
49
+ form.append('paperHeight', '11.7');
50
+ form.append('marginTop', '0.6');
51
+ form.append('marginBottom', '0.6');
52
+ form.append('marginLeft', '0.6');
53
+ form.append('marginRight', '0.6');
54
+ form.append('printBackground', 'true');
55
+ // Without this a callout's tint and a code block's ground are dropped and the page loses its shape.
56
+ form.append('preferCssPageSize', 'false');
57
+ if (options.title)
58
+ form.append('metadata', JSON.stringify({ Title: options.title, Creator: 'Kern Quire' }));
59
+ let response;
60
+ try {
61
+ response = await fetch(endpoint, {
62
+ method: 'POST',
63
+ body: form,
64
+ signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
65
+ });
66
+ }
67
+ catch (err) {
68
+ const why = err instanceof Error && err.name === 'TimeoutError' ? 'did not answer in time' : 'is not reachable';
69
+ throw new KernError('UNAVAILABLE', `PDF export needs Gotenberg, and ${base} ${why}. Start it (the self-host stack has it under ` +
70
+ '`--profile preview`) or point GOTENBERG_URL at one that is running.', { service: 'gotenberg', url: base });
71
+ }
72
+ if (!response.ok) {
73
+ const reason = await response.text().catch(() => '');
74
+ throw new KernError('UNAVAILABLE', `Gotenberg at ${base} refused to render this page (HTTP ${response.status}). ` +
75
+ `${reason.slice(0, REASON_LIMIT) || 'It gave no reason.'}`, { service: 'gotenberg', url: base, status: response.status });
76
+ }
77
+ const bytes = Buffer.from(await response.arrayBuffer());
78
+ /*
79
+ * A zero-length 200 is a real Gotenberg failure mode when Chromium dies mid-render, and it is the
80
+ * one that would otherwise be stored: an empty file, a job marked `done`, and a person who finds
81
+ * out by double-clicking it. `%PDF` is four bytes and settles it.
82
+ */
83
+ if (bytes.length === 0 || bytes.subarray(0, 4).toString('latin1') !== '%PDF')
84
+ throw new KernError('UNAVAILABLE', `Gotenberg at ${base} returned something that is not a PDF`, {
85
+ service: 'gotenberg',
86
+ url: base,
87
+ bytes: bytes.length,
88
+ });
89
+ return bytes;
90
+ }
91
+ //# sourceMappingURL=pdf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../../src/server/export/pdf.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ export declare function crc32(data: Uint8Array): number;
2
+ export interface ZipEntry {
3
+ /** the path inside the archive, `/`-separated and never absolute or `..`-relative */
4
+ path: string;
5
+ data: Uint8Array;
6
+ }
7
+ /** What a caller may not exceed, so a refusal can name the number it broke. */
8
+ export declare const ZIP_LIMITS: {
9
+ maxTotalBytes: number;
10
+ maxEntryBytes: number;
11
+ maxEntries: number;
12
+ };
13
+ /**
14
+ * A path that cannot escape the folder somebody unzips into.
15
+ *
16
+ * A page title becomes a folder name, and a title is user input: `../../etc/whatever` is a real
17
+ * archive somebody can be handed, and a tool that follows it writes outside the extraction
18
+ * directory. Every segment is cleaned rather than rejected, because refusing an export because
19
+ * somebody named a page `C:\` would be absurd — but the cleaning is total, so nothing survives that
20
+ * a reader could resolve upwards.
21
+ */
22
+ export declare function safeZipPath(path: string): string;
23
+ /**
24
+ * Every entry, in one buffer.
25
+ *
26
+ * Sizes are known before anything is written, so no entry carries a data descriptor and no reader
27
+ * has to seek backwards — which is what makes the output openable by the strictest tools as well as
28
+ * by the forgiving ones.
29
+ */
30
+ export declare function writeZip(entries: ZipEntry[], now?: Date): Buffer;
31
+ //# sourceMappingURL=zip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip.d.ts","sourceRoot":"","sources":["../../../src/server/export/zip.ts"],"names":[],"mappings":"AAqCA,wBAAgB,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAI9C;AAkBD,MAAM,WAAW,QAAQ;IACvB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,UAAU,CAAA;CACjB;AAED,+EAA+E;AAC/E,eAAO,MAAM,UAAU;;;;CAA4E,CAAA;AAEnG;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAchD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,GAAG,OAAa,GAAG,MAAM,CA4EtE"}