canopycms 0.6.0 → 0.7.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 (104) hide show
  1. package/CHANGELOG.md +174 -47
  2. package/dist/api/branch-review.d.ts +11 -1
  3. package/dist/api/branch-review.js +96 -18
  4. package/dist/api/branch-status.d.ts +26 -2
  5. package/dist/api/branch-status.js +38 -2
  6. package/dist/api/branch-withdraw.js +4 -7
  7. package/dist/api/branch.d.ts +2 -0
  8. package/dist/api/client.d.ts +3 -3
  9. package/dist/api/client.js +4 -4
  10. package/dist/api/comments.d.ts +2 -2
  11. package/dist/api/comments.js +4 -0
  12. package/dist/api/content.d.ts +2 -2
  13. package/dist/api/github-sync.d.ts +4 -0
  14. package/dist/api/github-sync.js +38 -6
  15. package/dist/api/pr-comment-sync.d.ts +18 -0
  16. package/dist/api/pr-comment-sync.js +77 -0
  17. package/dist/api/request-deadline.d.ts +15 -7
  18. package/dist/api/request-deadline.js +15 -7
  19. package/dist/api/schema.d.ts +16 -16
  20. package/dist/authorization/review-role.d.ts +33 -0
  21. package/dist/authorization/review-role.js +17 -0
  22. package/dist/branch-metadata-file.d.ts +3 -0
  23. package/dist/branch-metadata-file.js +1 -0
  24. package/dist/cli/cli.js +699 -130
  25. package/dist/cli/generate-ai-content.js +524 -59
  26. package/dist/cli/github-app-output.d.ts +34 -0
  27. package/dist/cli/github-app-output.js +85 -0
  28. package/dist/cli/init-github-app.d.ts +18 -0
  29. package/dist/cli/init-github-app.js +66 -58
  30. package/dist/cli/init.js +10 -1
  31. package/dist/comment-store.d.ts +5 -0
  32. package/dist/comment-store.js +24 -0
  33. package/dist/config/helpers.js +1 -3
  34. package/dist/config/schemas/collection.d.ts +2 -2
  35. package/dist/config/schemas/config.d.ts +3 -0
  36. package/dist/config/schemas/config.js +2 -1
  37. package/dist/config/schemas/url.d.ts +6 -0
  38. package/dist/config/schemas/url.js +16 -0
  39. package/dist/config/types.d.ts +12 -1
  40. package/dist/content-id-index.d.ts +9 -0
  41. package/dist/content-id-index.js +31 -0
  42. package/dist/editor/BranchManager.d.ts +3 -0
  43. package/dist/editor/BranchManager.js +75 -44
  44. package/dist/editor/CanopyEditor.js +2 -2
  45. package/dist/editor/CanopyEditorPage.js +3 -4
  46. package/dist/editor/Editor.js +47 -8
  47. package/dist/editor/branch-lock.d.ts +7 -0
  48. package/dist/editor/branch-lock.js +9 -2
  49. package/dist/editor/client-reference-resolver.d.ts +5 -3
  50. package/dist/editor/client-reference-resolver.js +10 -5
  51. package/dist/editor/components/BranchesLoadFailedNotice.d.ts +13 -0
  52. package/dist/editor/components/BranchesLoadFailedNotice.js +12 -0
  53. package/dist/editor/components/EditorHeader.d.ts +10 -4
  54. package/dist/editor/components/EditorHeader.js +108 -28
  55. package/dist/editor/components/EntryHeader.d.ts +5 -1
  56. package/dist/editor/components/EntryHeader.js +3 -1
  57. package/dist/editor/components/ReasonedMenuItem.d.ts +8 -0
  58. package/dist/editor/components/ReasonedMenuItem.js +2 -2
  59. package/dist/editor/components/ReviewDialogs.d.ts +19 -0
  60. package/dist/editor/components/ReviewDialogs.js +41 -0
  61. package/dist/editor/copy.d.ts +4 -0
  62. package/dist/editor/copy.js +4 -0
  63. package/dist/editor/fields/MarkdownField.js +1 -1
  64. package/dist/editor/hooks/create-branch-request.d.ts +6 -2
  65. package/dist/editor/hooks/create-branch-request.js +7 -2
  66. package/dist/editor/hooks/useBranchManager.d.ts +7 -2
  67. package/dist/editor/hooks/useBranchManager.js +252 -37
  68. package/dist/editor-url.d.ts +20 -0
  69. package/dist/editor-url.js +71 -0
  70. package/dist/git-manager.js +10 -6
  71. package/dist/github-service.d.ts +2 -2
  72. package/dist/github-service.js +4 -2
  73. package/dist/pr-comments.d.ts +33 -0
  74. package/dist/pr-comments.js +41 -0
  75. package/dist/schema/schema-store.d.ts +8 -8
  76. package/dist/services.d.ts +2 -17
  77. package/dist/services.js +38 -1
  78. package/dist/settings-workspace.d.ts +21 -7
  79. package/dist/settings-workspace.js +91 -16
  80. package/dist/submission-attribution.d.ts +38 -1
  81. package/dist/submission-attribution.js +197 -18
  82. package/dist/task-queue/cms-task-queue.d.ts +1 -1
  83. package/dist/types.d.ts +6 -0
  84. package/dist/utils/content-serialize.js +7 -6
  85. package/dist/utils/markdown-body-splice.d.ts +15 -10
  86. package/dist/utils/markdown-body-splice.js +71 -28
  87. package/dist/utils/markdown-export-options.d.ts +13 -0
  88. package/dist/utils/markdown-export-options.js +13 -0
  89. package/dist/utils/markdown-restyle.d.ts +58 -0
  90. package/dist/utils/markdown-restyle.js +482 -0
  91. package/dist/version.d.ts +1 -1
  92. package/dist/version.js +1 -1
  93. package/dist/worker/bundle-header.d.ts +13 -9
  94. package/dist/worker/bundle-header.js +17 -11
  95. package/dist/worker/git-process.d.ts +2 -2
  96. package/dist/worker/github-gateway-http.js +3 -2
  97. package/dist/worker/github-gateway-wire.d.ts +3 -0
  98. package/dist/worker/github-gateway-wire.js +1 -0
  99. package/dist/worker/github-gateway.d.ts +1 -1
  100. package/dist/worker/github-transfer.js +72 -2
  101. package/dist/worker/task-runner.js +53 -3
  102. package/package.json +2 -2
  103. package/dist/editor/fields/markdown-export-options.d.ts +0 -2
  104. package/dist/editor/fields/markdown-export-options.js +0 -8
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Restyles markdown to `MARKDOWN_EXPORT_OPTIONS`'s markers IN PLACE: each normalisation Prettier
3
+ * makes is an edit to the marker bytes of the text as typed, so every other byte (URLs, titles,
4
+ * entities, escapes, inline code, JSX) is the edit's own by construction. A serialiser cannot
5
+ * promise that: it writes every value from the tree, and a tree-equal rewrite of a URL or an
6
+ * entity is invisible to a tree check.
7
+ *
8
+ * The normalisations, each as Prettier 3 writes them:
9
+ * - emphasis `_`, or `*` where `_` could not delimit it (beside a word character) and inside
10
+ * other emphasis; strong `**`. Emphasis after a literal `_` in its block keeps `*`, because
11
+ * Prettier misreads `a_b _x_` (`underscoreStarts`);
12
+ * - bullets `-`, and ordered delimiters `.`, alternating with `*` / `)` for a list that follows
13
+ * another; ordered numbering by `orderedNumber`, where that keeps each marker's width;
14
+ * - `---` rules (`***` inside a list, see `ruleIn`), `# x` headings for one-line setext and ATX
15
+ * headings, backtick fences for `~~~`;
16
+ * - one blank line between blocks, none between link definitions; between list items and an
17
+ * item's blocks, several blank lines become one, which keeps a list's looseness;
18
+ * - no trailing space before a soft line ending; an escaped `_` between two letters or digits
19
+ * unescaped, except in links and in blocks holding `$` or `[[`;
20
+ * - tables padded to their columns, when every cell is printable ASCII.
21
+ *
22
+ * Each top-level block's edits are kept only if the text they are part of reads back as the
23
+ * edit (`readsBack`): all at once, then halves, within a parse budget, so a block that cannot be
24
+ * restyled leaves the others restyled.
25
+ */
26
+ import { MARKDOWN_EXPORT_OPTIONS } from './markdown-export-options.js';
27
+ const BULLETS = [MARKDOWN_EXPORT_OPTIONS.bullet, '*'];
28
+ const DELIMITERS = ['.', ')'];
29
+ const EMPHASIS = MARKDOWN_EXPORT_OPTIONS.emphasis;
30
+ const STRONG = MARKDOWN_EXPORT_OPTIONS.strong.repeat(2);
31
+ const RULE = MARKDOWN_EXPORT_OPTIONS.rule.repeat(3);
32
+ /** A character Prettier counts as part of a word: neither whitespace nor punctuation. */
33
+ const WORD_CHAR = /[^\s\p{P}\p{S}]/u;
34
+ /** Where a block's text ends and the next one's begins: whitespace, and blockquote markers. */
35
+ const GAP = /^[ \t>]*(?:\r?\n[ \t>]*)+$/;
36
+ function startOf(node) {
37
+ return node?.position?.start.offset;
38
+ }
39
+ function endOf(node) {
40
+ return node?.position?.end.offset;
41
+ }
42
+ function apply(source, edits, from = 0, to = source.length) {
43
+ let out = '';
44
+ let at = from;
45
+ for (const edit of [...edits].sort((a, b) => a.start - b.start)) {
46
+ if (edit.start < at || edit.end > to)
47
+ continue;
48
+ out += source.slice(at, edit.start) + edit.text;
49
+ at = edit.end;
50
+ }
51
+ return out + source.slice(at, to);
52
+ }
53
+ /**
54
+ * Where each text or MDX expression under `node` that holds a literal `_` starts, outside
55
+ * bracketed links: Prettier reads `[a_b](u) _x_` and `<https://x/a_b> _x_` correctly, but a
56
+ * bare URL only when its host has a dot, so bare URLs count.
57
+ */
58
+ function underscoreStarts(node) {
59
+ const bare = startOf(node) === startOf(node.children?.[0]);
60
+ if ((node.type === 'link' && !bare) || node.type === 'linkReference')
61
+ return [];
62
+ if (node.type === 'text' || node.type === 'mdxTextExpression') {
63
+ const start = startOf(node);
64
+ return start !== undefined && (node.value ?? '').includes('_') ? [start] : [];
65
+ }
66
+ return (node.children ?? []).flatMap(underscoreStarts);
67
+ }
68
+ /**
69
+ * The number Prettier writes for item `index` of an ordered list typed with `numbers`: the first
70
+ * keeps its number; the rest count up from it, or are all 1 when the list is typed that way
71
+ * (its second number is 1, and so is its third when it starts at 0).
72
+ */
73
+ export function orderedNumber(numbers, index) {
74
+ const [first = 1, second, third] = numbers;
75
+ if (index === 0)
76
+ return first;
77
+ const ones = numbers.length > 1 && second === 1 && (first !== 0 || numbers.length < 3 || third === 1);
78
+ return ones ? 1 : first + index;
79
+ }
80
+ class Restyler {
81
+ constructor(source) {
82
+ this.source = source;
83
+ /** The marker each list is written with, its own when it is not restyled. */
84
+ this.listMarker = new Map();
85
+ /** Per block that holds phrasing, where its text nodes with a literal `_` start. */
86
+ this.underscores = new Map();
87
+ }
88
+ /** Edits for `node` and everything under it, given its ancestors (nearest last). */
89
+ collect(node, ancestors, out) {
90
+ const inside = [...ancestors, node];
91
+ switch (node.type) {
92
+ case 'emphasis':
93
+ this.emphasis(node, ancestors, out);
94
+ break;
95
+ case 'strong':
96
+ this.strong(node, out);
97
+ break;
98
+ case 'text':
99
+ this.text(node, ancestors, out);
100
+ return;
101
+ case 'thematicBreak':
102
+ this.replaceWhole(node, this.ruleIn(ancestors), out);
103
+ return;
104
+ case 'code':
105
+ this.fence(node, out);
106
+ return;
107
+ case 'heading':
108
+ this.heading(node, inside, out);
109
+ return;
110
+ case 'table':
111
+ this.table(node, inside, out);
112
+ return;
113
+ default:
114
+ break;
115
+ }
116
+ this.children(node, inside, out);
117
+ }
118
+ /**
119
+ * Edits for `parent`'s children and the gaps between them. `fixed` names children whose text
120
+ * must stay as it is (untouched top-level blocks); their lists still count for alternation.
121
+ */
122
+ children(parent, inside, out, fixed = () => false, outFor = () => out) {
123
+ const children = parent.children ?? [];
124
+ const markers = this.listMarkers(children, fixed);
125
+ children.forEach((child, index) => {
126
+ if (fixed(index))
127
+ return;
128
+ const edits = outFor(index);
129
+ const marker = markers.get(child);
130
+ if (marker !== undefined)
131
+ this.list(child, marker, edits);
132
+ this.collect(child, inside, edits);
133
+ });
134
+ const always = parent.type === 'root' || parent.type === 'blockquote';
135
+ const spaced = always || parent.type === 'list' || parent.type === 'listItem';
136
+ if (!spaced)
137
+ return;
138
+ for (let index = 1; index < children.length; index++) {
139
+ if (fixed(index) && fixed(index - 1))
140
+ continue;
141
+ this.gap(children[index - 1], children[index], always, outFor(fixed(index) ? index - 1 : index));
142
+ }
143
+ }
144
+ /** The marker each list among `children` takes: its own, then alternating with the one before. */
145
+ listMarkers(children, fixed) {
146
+ const markers = new Map();
147
+ let previous;
148
+ children.forEach((child, index) => {
149
+ if (child.type !== 'list') {
150
+ previous = undefined;
151
+ return;
152
+ }
153
+ const ordered = Boolean(child.ordered);
154
+ const current = this.markerOf(child);
155
+ if (current === undefined) {
156
+ previous = undefined;
157
+ return;
158
+ }
159
+ let marker = current;
160
+ if (!fixed(index)) {
161
+ const choices = ordered ? DELIMITERS : BULLETS;
162
+ const next = children[index + 1];
163
+ const nextFixed = next?.type === 'list' && Boolean(next.ordered) === ordered && fixed(index + 1)
164
+ ? this.markerOf(next)
165
+ : undefined;
166
+ const before = previous?.ordered === ordered ? previous.marker : undefined;
167
+ marker = choices.find((choice) => choice !== before && choice !== nextFixed) ?? current;
168
+ markers.set(child, marker);
169
+ }
170
+ this.listMarker.set(child, marker);
171
+ previous = { ordered, marker };
172
+ });
173
+ return markers;
174
+ }
175
+ /**
176
+ * A rule's text: `---`, but inside a list Prettier writes `***`, or `---` when the list takes
177
+ * the alternate marker.
178
+ */
179
+ ruleIn(ancestors) {
180
+ const list = [...ancestors].reverse().find((a) => a.type === 'list');
181
+ if (list === undefined)
182
+ return RULE;
183
+ const marker = this.listMarker.get(list);
184
+ return marker === BULLETS[0] || marker === DELIMITERS[0] ? '***' : RULE;
185
+ }
186
+ /** A list's bullet, or its ordered delimiter, read from its first item. */
187
+ markerOf(list) {
188
+ const at = startOf(list.children?.[0]);
189
+ if (at === undefined)
190
+ return undefined;
191
+ if (!list.ordered)
192
+ return this.source[at];
193
+ return /^\d{1,9}([.)])/.exec(this.source.slice(at, at + 10))?.[1];
194
+ }
195
+ /** Item markers: the list's bullet or delimiter, and Prettier's numbering where widths allow. */
196
+ list(list, marker, out) {
197
+ const items = list.children ?? [];
198
+ const starts = items.map(startOf);
199
+ if (!list.ordered) {
200
+ for (const at of starts) {
201
+ if (at !== undefined && this.source[at] !== marker) {
202
+ out.push({ start: at, end: at + 1, text: marker });
203
+ }
204
+ }
205
+ return;
206
+ }
207
+ const numbers = starts.map((at) => at === undefined ? null : /^(\d{1,9})[.)]/.exec(this.source.slice(at, at + 11)));
208
+ const typed = numbers.map((match) => Number(match?.[1]));
209
+ const renumber = numbers.every((match, i) => match !== null && String(orderedNumber(typed, i)).length === match[1]?.length);
210
+ numbers.forEach((match, i) => {
211
+ const at = starts[i];
212
+ if (match === null || at === undefined)
213
+ return;
214
+ const digits = match[1] ?? '';
215
+ const wanted = renumber ? String(orderedNumber(typed, i)) : digits;
216
+ if (wanted !== digits || match[0].slice(-1) !== marker) {
217
+ out.push({ start: at, end: at + match[0].length, text: wanted + marker });
218
+ }
219
+ });
220
+ }
221
+ /**
222
+ * The gap between two siblings with exactly one blank line, kept in the container's own
223
+ * prefix and line ending; where `always` is false, only a gap that had a blank line.
224
+ */
225
+ gap(a, b, always, out) {
226
+ const end = this.contentEnd(a);
227
+ const start = startOf(b);
228
+ if (end === undefined || start === undefined)
229
+ return;
230
+ const gap = this.source.slice(end, start);
231
+ if (!GAP.test(gap))
232
+ return;
233
+ const lines = gap.split(/\r?\n/);
234
+ if (!always && lines.length < 3)
235
+ return;
236
+ const eol = gap.includes('\r\n') ? '\r\n' : '\n';
237
+ const prefix = lines[lines.length - 1] ?? '';
238
+ // Prettier keeps consecutive link reference definitions together.
239
+ const together = a?.type === 'definition' && b?.type === 'definition';
240
+ const text = together ? eol + prefix : eol + prefix.trimEnd() + eol + prefix;
241
+ if (text !== gap)
242
+ out.push({ start: end, end: start, text });
243
+ }
244
+ /**
245
+ * Where `node`'s last line of content ends: a list's position runs on over the blank lines
246
+ * after it, which in a block quote are lines of `>`.
247
+ */
248
+ contentEnd(node) {
249
+ const start = startOf(node);
250
+ let end = endOf(node);
251
+ if (start === undefined || end === undefined)
252
+ return end;
253
+ for (;;) {
254
+ const lineStart = this.source.lastIndexOf('\n', end - 1);
255
+ if (lineStart <= start || !/^[ \t>]*$/.test(this.source.slice(lineStart + 1, end)))
256
+ break;
257
+ end = this.source[lineStart - 1] === '\r' ? lineStart - 1 : lineStart;
258
+ }
259
+ return end;
260
+ }
261
+ emphasis(node, ancestors, out) {
262
+ const start = startOf(node);
263
+ const end = endOf(node);
264
+ if (start === undefined || end === undefined)
265
+ return;
266
+ const open = this.source[start];
267
+ if (open !== this.source[end - 1] || (open !== '*' && open !== '_'))
268
+ return;
269
+ const marker = ancestors.some((a) => a.type === 'emphasis') ||
270
+ this.besideWord(start, end) ||
271
+ this.afterUnderscore(start, ancestors)
272
+ ? '*'
273
+ : EMPHASIS;
274
+ if (open === marker)
275
+ return;
276
+ out.push({ start, end: start + 1, text: marker }, { start: end - 1, end, text: marker });
277
+ }
278
+ strong(node, out) {
279
+ const start = startOf(node);
280
+ const end = endOf(node);
281
+ if (start === undefined || end === undefined)
282
+ return;
283
+ if (this.source.slice(start, start + 2) !== '__' || this.source.slice(end - 2, end) !== '__') {
284
+ return;
285
+ }
286
+ out.push({ start, end: start + 2, text: STRONG }, { start: end - 2, end, text: STRONG });
287
+ }
288
+ /** Whether the characters past the markers around `[start, end)` include a word character. */
289
+ besideWord(start, end) {
290
+ const markers = '*_';
291
+ let before = start - 1;
292
+ while (before >= 0 && markers.includes(this.source[before] ?? ''))
293
+ before--;
294
+ let after = end;
295
+ while (after < this.source.length && markers.includes(this.source[after] ?? ''))
296
+ after++;
297
+ return WORD_CHAR.test(this.source[before] ?? ' ') || WORD_CHAR.test(this.source[after] ?? ' ');
298
+ }
299
+ afterUnderscore(start, ancestors) {
300
+ const block = [...ancestors]
301
+ .reverse()
302
+ .find((a) => a.type === 'paragraph' || a.type === 'heading' || a.type === 'tableCell');
303
+ if (block === undefined)
304
+ return false;
305
+ let starts = this.underscores.get(block);
306
+ if (starts === undefined) {
307
+ starts = underscoreStarts(block);
308
+ this.underscores.set(block, starts);
309
+ }
310
+ // Text in the link the emphasis is in counts as the block's own.
311
+ const link = [...ancestors]
312
+ .reverse()
313
+ .find((a) => a.type === 'link' || a.type === 'linkReference');
314
+ const inLink = (link?.children ?? []).flatMap(underscoreStarts);
315
+ return [...starts, ...inLink].some((at) => at < start);
316
+ }
317
+ text(node, ancestors, out) {
318
+ const start = startOf(node);
319
+ const end = endOf(node);
320
+ if (start === undefined || end === undefined)
321
+ return;
322
+ const raw = this.source.slice(start, end);
323
+ for (const match of raw.matchAll(/[ \t]+(?=\r?\n)/g)) {
324
+ out.push({ start: start + match.index, end: start + match.index + match[0].length, text: '' });
325
+ }
326
+ if (ancestors.some((a) => a.type === 'link' || a.type === 'linkReference'))
327
+ return;
328
+ // In `$` math and `[[wiki links]]`, which plugins read raw, `\_` and `_` differ.
329
+ const block = [...ancestors]
330
+ .reverse()
331
+ .find((a) => a.type === 'paragraph' || a.type === 'heading' || a.type === 'tableCell');
332
+ const blockStart = startOf(block);
333
+ const blockEnd = endOf(block);
334
+ if (blockStart === undefined || blockEnd === undefined)
335
+ return;
336
+ if (/\$|\[\[/.test(this.source.slice(blockStart, blockEnd)))
337
+ return;
338
+ for (const match of raw.matchAll(/(?<=[\p{L}\p{N}])\\_(?=[\p{L}\p{N}])/gu)) {
339
+ out.push({ start: start + match.index, end: start + match.index + 1, text: '' });
340
+ }
341
+ }
342
+ replaceWhole(node, text, out) {
343
+ const start = startOf(node);
344
+ const end = endOf(node);
345
+ if (start === undefined || end === undefined)
346
+ return;
347
+ if (this.source.slice(start, end) !== text)
348
+ out.push({ start, end, text });
349
+ }
350
+ /** A `~~~` fence as backticks, when nothing inside it would end a backtick fence. */
351
+ fence(node, out) {
352
+ const start = startOf(node);
353
+ const end = endOf(node);
354
+ if (start === undefined || end === undefined)
355
+ return;
356
+ const raw = this.source.slice(start, end);
357
+ const close = /(?:\r?\n)[ \t>]*(~{3,})[ \t]*$/.exec(raw);
358
+ if (!/^~~~(?!~)[^`\n]*\n/.test(raw) || close?.[1] === undefined || raw.includes('`'))
359
+ return;
360
+ const closeAt = start + close.index + close[0].indexOf('~');
361
+ out.push({ start, end: start + 3, text: '```' }, { start: closeAt, end: closeAt + close[1].length, text: '```' });
362
+ }
363
+ /** A one-line heading as `# x`, its content with its own edits. */
364
+ heading(node, inside, out) {
365
+ const inner = [];
366
+ this.children(node, inside, inner);
367
+ const children = node.children ?? [];
368
+ const start = startOf(node);
369
+ const end = endOf(node);
370
+ const contentStart = startOf(children[0]);
371
+ const contentEnd = endOf(children[children.length - 1]);
372
+ if (start === undefined ||
373
+ end === undefined ||
374
+ contentStart === undefined ||
375
+ contentEnd === undefined ||
376
+ /[\r\n]/.test(this.source.slice(contentStart, contentEnd))) {
377
+ out.push(...inner);
378
+ return;
379
+ }
380
+ const content = apply(this.source, inner, contentStart, contentEnd);
381
+ const text = `${'#'.repeat(node.depth ?? 1)} ${content}`;
382
+ if (text !== this.source.slice(start, end))
383
+ out.push({ start, end, text });
384
+ else
385
+ out.push(...inner);
386
+ }
387
+ /** A table padded to its columns, as Prettier pads it; its cells keep their own text. */
388
+ table(node, inside, out) {
389
+ const start = startOf(node);
390
+ const end = endOf(node);
391
+ if (start === undefined || end === undefined)
392
+ return;
393
+ const prefix = this.source.slice(this.source.lastIndexOf('\n', start - 1) + 1, start);
394
+ const inner = [];
395
+ const rows = (node.children ?? []).map((row) => (row.children ?? []).map((cell) => {
396
+ const edits = [];
397
+ this.children(cell, [...inside, row, cell], edits);
398
+ inner.push(...edits);
399
+ const content = cell.children ?? [];
400
+ const from = startOf(content[0]);
401
+ const to = endOf(content[content.length - 1]);
402
+ return from === undefined || to === undefined ? '' : apply(this.source, edits, from, to);
403
+ }));
404
+ // Prettier pads by display width, which counts wide and emoji characters twice.
405
+ const plain = rows.every((row) => row.every((cell) => /^[\x20-\x7e]*$/.test(cell)));
406
+ if (!plain || !/^[ \t>]*$/.test(prefix)) {
407
+ out.push(...inner);
408
+ return;
409
+ }
410
+ const align = node.align ?? [];
411
+ const columns = Math.max(...rows.map((row) => row.length), align.length);
412
+ const widths = Array.from({ length: columns }, (_, c) => Math.max(3, ...rows.map((row) => row[c]?.length ?? 0)));
413
+ const pad = (text, c) => {
414
+ const room = (widths[c] ?? 3) - text.length;
415
+ if (align[c] === 'right')
416
+ return ' '.repeat(room) + text;
417
+ if (align[c] === 'center') {
418
+ const left = Math.floor(room / 2);
419
+ return ' '.repeat(left) + text + ' '.repeat(room - left);
420
+ }
421
+ return text + ' '.repeat(room);
422
+ };
423
+ const delimiter = widths.map((width, c) => {
424
+ const a = align[c];
425
+ if (a === 'left')
426
+ return `:${'-'.repeat(width - 1)}`;
427
+ if (a === 'right')
428
+ return `${'-'.repeat(width - 1)}:`;
429
+ if (a === 'center')
430
+ return `:${'-'.repeat(width - 2)}:`;
431
+ return '-'.repeat(width);
432
+ });
433
+ const line = (cells) => `| ${cells.join(' | ')} |`;
434
+ const lines = rows.map((row) => line(row.map(pad)));
435
+ lines.splice(1, 0, line(delimiter));
436
+ const eol = this.source.slice(start, end).includes('\r\n') ? '\r\n' : '\n';
437
+ const text = lines.join(eol + prefix);
438
+ if (text !== this.source.slice(start, end))
439
+ out.push({ start, end, text });
440
+ }
441
+ }
442
+ /**
443
+ * `source` (whose parse is `tree`) restyled in place, every top-level block for which `fixed`
444
+ * is false; undefined when no restyle reads back. `readsBack` returns the parse of a candidate
445
+ * when it means what `source` means.
446
+ */
447
+ export function restyleMarkdown(source, tree, fixed, readsBack) {
448
+ // Positions do not count a byte-order mark, so edits would land one byte early.
449
+ if (source.startsWith('\uFEFF'))
450
+ return undefined;
451
+ const blocks = tree.children ?? [];
452
+ const groups = blocks.map(() => []);
453
+ new Restyler(source).children(tree, [], [], fixed, (index) => groups[index] ?? []);
454
+ const changed = groups.filter((group) => group.length > 0);
455
+ if (changed.length === 0)
456
+ return undefined;
457
+ // The whole restyle first, then halves of it, until each block that does not read back stands
458
+ // alone: about two parses per such block per halving, so the budget covers about five of them
459
+ // spread through 200 blocks. Each parse reads the whole body under the write lock, so past the
460
+ // budget the restyle keeps what has read back.
461
+ let kept = [];
462
+ let best;
463
+ let budget = 8 * Math.ceil(Math.log2(changed.length + 1)) + 8;
464
+ const attempt = (tried) => {
465
+ if (budget-- <= 0)
466
+ return;
467
+ const trial = [...kept, ...tried.flat()];
468
+ const text = apply(source, trial);
469
+ const parsed = readsBack(text);
470
+ if (parsed !== undefined) {
471
+ kept = trial;
472
+ best = { text, tree: parsed };
473
+ }
474
+ else if (tried.length > 1) {
475
+ const half = Math.ceil(tried.length / 2);
476
+ attempt(tried.slice(0, half));
477
+ attempt(tried.slice(half));
478
+ }
479
+ };
480
+ attempt(changed);
481
+ return best;
482
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const CANOPYCMS_VERSION = "0.6.0";
1
+ export declare const CANOPYCMS_VERSION = "0.7.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/bump-version.mjs, its only writer. The package builds with plain
2
2
  // tsc (rootDir: src), so src/ cannot import ../package.json; version.test.ts keeps the two equal.
3
- export const CANOPYCMS_VERSION = '0.6.0';
3
+ export const CANOPYCMS_VERSION = '0.7.0';
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * A git bundle's header, read in node before git sees the bundle: the gateway refuses a pushed
3
- * bundle on it (worker/github-mirror.ts), and the worker checks the bundle it is about to send
4
- * (worker/github-transfer.ts). git's own parser reads everything after a head's first space as the
5
- * refname and accepts a tab, so nothing here is lenient: a header either matches this grammar
6
- * exactly or is refused.
3
+ * bundle on it (worker/github-mirror.ts), and the worker checks the bundle it is about to send and
4
+ * reads the prerequisites of one it takes in (worker/github-transfer.ts). git's own parser reads
5
+ * everything after a head's first space as the refname and accepts a tab, so nothing here is
6
+ * lenient: a header either matches this grammar exactly or is refused.
7
7
  *
8
8
  * ```
9
9
  * "# v2 git bundle" LF | "# v3 git bundle" LF ("@object-format=sha1" LF)? at most once
@@ -24,7 +24,7 @@
24
24
  */
25
25
  /**
26
26
  * The header must end, with its blank line, within this many bytes, prerequisites' comments not
27
- * counted.
27
+ * counted, unless the reader is given another bound.
28
28
  * @internal Exported for tests.
29
29
  */
30
30
  export declare const BUNDLE_HEADER_MAX_BYTES: number;
@@ -35,7 +35,8 @@ export declare const BUNDLE_HEADER_MAX_BYTES: number;
35
35
  export declare const PREREQUISITE_COMMENT_MAX_BYTES = 1024;
36
36
  /**
37
37
  * The header must end within this many bytes counting comments too: the bytes read, when
38
- * refusing; the bytes kept and sent, when shortening. The most a reader holds of a header.
38
+ * refusing; the bytes kept and sent, when shortening. The most a refusing or shortening reader
39
+ * holds of a header.
39
40
  * @internal Exported for tests.
40
41
  */
41
42
  export declare const BUNDLE_HEADER_TOTAL_MAX_BYTES: number;
@@ -61,13 +62,16 @@ export interface ReadBundleHeader {
61
62
  }
62
63
  /**
63
64
  * Reads a header from a bundle's bytes as they arrive, holding at most
64
- * {@link BUNDLE_HEADER_TOTAL_MAX_BYTES} of it, however long a comment is. A comment over
65
+ * {@link BUNDLE_HEADER_TOTAL_MAX_BYTES} of it (`maxBytes`, skipping), however long a comment is.
66
+ * A comment over
65
67
  * {@link PREREQUISITE_COMMENT_MAX_BYTES} is refused (`'refuse'`, the gateway, which keeps no copy
66
68
  * of the header) or cut to it (`'shorten'`, the worker, which sends {@link ReadBundleHeader.bytes}
67
- * in its place).
69
+ * in its place). `'skip'` keeps no comment byte at all and bounds only `maxBytes`, for a bundle
70
+ * only read, never sent on.
68
71
  */
69
72
  export declare class BundleHeaderReader {
70
73
  private readonly comments;
74
+ private readonly maxBytes;
71
75
  private section;
72
76
  private sawCapability;
73
77
  private readonly prerequisites;
@@ -85,7 +89,7 @@ export declare class BundleHeaderReader {
85
89
  /** In a comment: its bytes so far, and those kept to send. */
86
90
  private comment;
87
91
  private done;
88
- constructor(comments: 'refuse' | 'shorten');
92
+ constructor(comments: 'refuse' | 'shorten' | 'skip', maxBytes?: number);
89
93
  /**
90
94
  * Take the next bytes: null while the header may still be completed by more, the header once its
91
95
  * blank line has arrived. Throws {@link BundleHeaderError} once what has arrived breaks the
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * A git bundle's header, read in node before git sees the bundle: the gateway refuses a pushed
3
- * bundle on it (worker/github-mirror.ts), and the worker checks the bundle it is about to send
4
- * (worker/github-transfer.ts). git's own parser reads everything after a head's first space as the
5
- * refname and accepts a tab, so nothing here is lenient: a header either matches this grammar
6
- * exactly or is refused.
3
+ * bundle on it (worker/github-mirror.ts), and the worker checks the bundle it is about to send and
4
+ * reads the prerequisites of one it takes in (worker/github-transfer.ts). git's own parser reads
5
+ * everything after a head's first space as the refname and accepts a tab, so nothing here is
6
+ * lenient: a header either matches this grammar exactly or is refused.
7
7
  *
8
8
  * ```
9
9
  * "# v2 git bundle" LF | "# v3 git bundle" LF ("@object-format=sha1" LF)? at most once
@@ -24,7 +24,7 @@
24
24
  */
25
25
  /**
26
26
  * The header must end, with its blank line, within this many bytes, prerequisites' comments not
27
- * counted.
27
+ * counted, unless the reader is given another bound.
28
28
  * @internal Exported for tests.
29
29
  */
30
30
  export const BUNDLE_HEADER_MAX_BYTES = 64 * 1024;
@@ -35,7 +35,8 @@ export const BUNDLE_HEADER_MAX_BYTES = 64 * 1024;
35
35
  export const PREREQUISITE_COMMENT_MAX_BYTES = 1024;
36
36
  /**
37
37
  * The header must end within this many bytes counting comments too: the bytes read, when
38
- * refusing; the bytes kept and sent, when shortening. The most a reader holds of a header.
38
+ * refusing; the bytes kept and sent, when shortening. The most a refusing or shortening reader
39
+ * holds of a header.
39
40
  * @internal Exported for tests.
40
41
  */
41
42
  export const BUNDLE_HEADER_TOTAL_MAX_BYTES = 256 * 1024;
@@ -57,14 +58,17 @@ const DASH = 0x2d;
57
58
  const PREREQUISITE_PREFIX_BYTES = 42;
58
59
  /**
59
60
  * Reads a header from a bundle's bytes as they arrive, holding at most
60
- * {@link BUNDLE_HEADER_TOTAL_MAX_BYTES} of it, however long a comment is. A comment over
61
+ * {@link BUNDLE_HEADER_TOTAL_MAX_BYTES} of it (`maxBytes`, skipping), however long a comment is.
62
+ * A comment over
61
63
  * {@link PREREQUISITE_COMMENT_MAX_BYTES} is refused (`'refuse'`, the gateway, which keeps no copy
62
64
  * of the header) or cut to it (`'shorten'`, the worker, which sends {@link ReadBundleHeader.bytes}
63
- * in its place).
65
+ * in its place). `'skip'` keeps no comment byte at all and bounds only `maxBytes`, for a bundle
66
+ * only read, never sent on.
64
67
  */
65
68
  export class BundleHeaderReader {
66
- constructor(comments) {
69
+ constructor(comments, maxBytes = BUNDLE_HEADER_MAX_BYTES) {
67
70
  this.comments = comments;
71
+ this.maxBytes = maxBytes;
68
72
  this.section = 'signature';
69
73
  this.sawCapability = false;
70
74
  this.prerequisites = [];
@@ -167,9 +171,11 @@ export class BundleHeaderReader {
167
171
  this.checkBound();
168
172
  }
169
173
  checkBound() {
170
- if (this.counted >= BUNDLE_HEADER_MAX_BYTES) {
171
- throw new BundleHeaderError(`its header does not end within ${BUNDLE_HEADER_MAX_BYTES} bytes`);
174
+ if (this.counted >= this.maxBytes) {
175
+ throw new BundleHeaderError(`its header does not end within ${this.maxBytes} bytes`);
172
176
  }
177
+ if (this.comments === 'skip')
178
+ return;
173
179
  // Refusing, every comment byte is read; shortening, only the kept ones are held and sent, so
174
180
  // a single subject of any length shortens rather than counts.
175
181
  const total = this.comments === 'refuse' ? this.consumed : this.counted + this.keptBytes;
@@ -1,7 +1,7 @@
1
1
  import { Readable } from 'node:stream';
2
2
  /**
3
- * git with its stdin or stdout as a stream, which simple-git cannot do: a bundle's way into
4
- * `bundle unbundle -` and `fetch`, out of `bundle create -`, and `update-ref --stdin`'s
3
+ * git with its stdin or stdout as a stream, which simple-git cannot do: a bundle's pack into
4
+ * `index-pack --stdin`, a bundle into `fetch`, out of `bundle create -`, and `update-ref --stdin`'s
5
5
  * transactions. The caller supplies the whole environment, config and working directory, so the
6
6
  * pins of the repository it runs in apply here exactly as they do through simple-git
7
7
  * (worker/shared-repo-git.ts, worker/github-mirror.ts).
@@ -219,8 +219,9 @@ class GatewayRequestHandler {
219
219
  return {};
220
220
  },
221
221
  getPullRequest: async ({ number }, signal) => {
222
- const { merged, merged_at, state, node_id } = await gateway.getPullRequest(number, signal);
223
- return { merged, merged_at, state, node_id };
222
+ const { merged, merged_at, state, node_id, body } = await gateway.getPullRequest(number, signal);
223
+ // Octokit's type says string | null; a payload without the field crosses as null.
224
+ return { merged, merged_at, state, node_id, body: body ?? null };
224
225
  },
225
226
  createOrUpdatePullRequest: async (input, signal) => {
226
227
  const { number, url, created } = await gateway.createOrUpdatePullRequest(input, signal);
@@ -190,14 +190,17 @@ export declare const answers: {
190
190
  merged_at: z.ZodNullable<z.ZodString>;
191
191
  state: z.ZodEnum<["open", "closed"]>;
192
192
  node_id: z.ZodString;
193
+ body: z.ZodNullable<z.ZodString>;
193
194
  }, "strict", z.ZodTypeAny, {
194
195
  merged: boolean;
195
196
  state: "open" | "closed";
197
+ body: string | null;
196
198
  merged_at: string | null;
197
199
  node_id: string;
198
200
  }, {
199
201
  merged: boolean;
200
202
  state: "open" | "closed";
203
+ body: string | null;
201
204
  merged_at: string | null;
202
205
  node_id: string;
203
206
  }>;
@@ -107,6 +107,7 @@ export const answers = {
107
107
  merged_at: z.string().max(64).nullable(),
108
108
  state: z.enum(['open', 'closed']),
109
109
  node_id: z.string().max(256),
110
+ body: pullRequestText.body.nullable(),
110
111
  })
111
112
  .strict(),
112
113
  createOrUpdatePullRequest: pullRequestRef.extend({ created: z.boolean() }).strict(),