canopycms 0.5.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.
- package/CHANGELOG.md +185 -46
- package/dist/api/branch-review.d.ts +11 -1
- package/dist/api/branch-review.js +96 -18
- package/dist/api/branch-status.d.ts +26 -2
- package/dist/api/branch-status.js +38 -2
- package/dist/api/branch-withdraw.js +4 -7
- package/dist/api/branch.d.ts +2 -0
- package/dist/api/client.d.ts +3 -3
- package/dist/api/client.js +4 -4
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/comments.js +4 -0
- package/dist/api/content.d.ts +2 -2
- package/dist/api/github-sync.d.ts +4 -0
- package/dist/api/github-sync.js +38 -6
- package/dist/api/pr-comment-sync.d.ts +18 -0
- package/dist/api/pr-comment-sync.js +77 -0
- package/dist/api/request-deadline.d.ts +15 -7
- package/dist/api/request-deadline.js +15 -7
- package/dist/api/schema.d.ts +16 -16
- package/dist/authorization/review-role.d.ts +33 -0
- package/dist/authorization/review-role.js +17 -0
- package/dist/branch-metadata-file.d.ts +3 -0
- package/dist/branch-metadata-file.js +1 -0
- package/dist/cli/cli.js +704 -131
- package/dist/cli/generate-ai-content.js +524 -59
- package/dist/cli/github-app-output.d.ts +34 -0
- package/dist/cli/github-app-output.js +85 -0
- package/dist/cli/init-github-app.d.ts +18 -0
- package/dist/cli/init-github-app.js +66 -58
- package/dist/cli/init.js +15 -2
- package/dist/cli/template-files/cms-stack.ts.template +3 -0
- package/dist/cli/templates.js +5 -1
- package/dist/comment-store.d.ts +5 -0
- package/dist/comment-store.js +24 -0
- package/dist/config/helpers.js +1 -3
- package/dist/config/schemas/collection.d.ts +2 -2
- package/dist/config/schemas/config.d.ts +3 -0
- package/dist/config/schemas/config.js +2 -1
- package/dist/config/schemas/url.d.ts +6 -0
- package/dist/config/schemas/url.js +16 -0
- package/dist/config/types.d.ts +12 -1
- package/dist/content-id-index.d.ts +9 -0
- package/dist/content-id-index.js +31 -0
- package/dist/editor/BranchManager.d.ts +3 -0
- package/dist/editor/BranchManager.js +75 -44
- package/dist/editor/CanopyEditor.js +2 -2
- package/dist/editor/CanopyEditorPage.js +3 -4
- package/dist/editor/Editor.js +47 -8
- package/dist/editor/branch-lock.d.ts +7 -0
- package/dist/editor/branch-lock.js +9 -2
- package/dist/editor/client-reference-resolver.d.ts +5 -3
- package/dist/editor/client-reference-resolver.js +10 -5
- package/dist/editor/components/BranchesLoadFailedNotice.d.ts +13 -0
- package/dist/editor/components/BranchesLoadFailedNotice.js +12 -0
- package/dist/editor/components/EditorHeader.d.ts +10 -4
- package/dist/editor/components/EditorHeader.js +108 -28
- package/dist/editor/components/EntryHeader.d.ts +5 -1
- package/dist/editor/components/EntryHeader.js +3 -1
- package/dist/editor/components/ReasonedMenuItem.d.ts +8 -0
- package/dist/editor/components/ReasonedMenuItem.js +2 -2
- package/dist/editor/components/ReviewDialogs.d.ts +19 -0
- package/dist/editor/components/ReviewDialogs.js +41 -0
- package/dist/editor/copy.d.ts +4 -0
- package/dist/editor/copy.js +4 -0
- package/dist/editor/fields/MarkdownField.js +1 -1
- package/dist/editor/hooks/create-branch-request.d.ts +6 -2
- package/dist/editor/hooks/create-branch-request.js +7 -2
- package/dist/editor/hooks/useBranchManager.d.ts +7 -2
- package/dist/editor/hooks/useBranchManager.js +252 -37
- package/dist/editor-url.d.ts +20 -0
- package/dist/editor-url.js +71 -0
- package/dist/git-manager.js +10 -6
- package/dist/github-service.d.ts +2 -2
- package/dist/github-service.js +4 -2
- package/dist/pr-comments.d.ts +33 -0
- package/dist/pr-comments.js +41 -0
- package/dist/schema/schema-store.d.ts +8 -8
- package/dist/services.d.ts +2 -17
- package/dist/services.js +38 -1
- package/dist/settings-workspace.d.ts +21 -7
- package/dist/settings-workspace.js +91 -16
- package/dist/submission-attribution.d.ts +38 -1
- package/dist/submission-attribution.js +197 -18
- package/dist/task-queue/cms-task-queue.d.ts +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/utils/content-serialize.js +7 -6
- package/dist/utils/markdown-body-splice.d.ts +15 -10
- package/dist/utils/markdown-body-splice.js +71 -28
- package/dist/utils/markdown-export-options.d.ts +13 -0
- package/dist/utils/markdown-export-options.js +13 -0
- package/dist/utils/markdown-restyle.d.ts +58 -0
- package/dist/utils/markdown-restyle.js +482 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/bundle-header.d.ts +13 -9
- package/dist/worker/bundle-header.js +17 -11
- package/dist/worker/git-process.d.ts +2 -2
- package/dist/worker/github-gateway-http.js +5 -4
- package/dist/worker/github-gateway-wire.d.ts +3 -0
- package/dist/worker/github-gateway-wire.js +1 -0
- package/dist/worker/github-gateway.d.ts +1 -1
- package/dist/worker/github-gateway.js +2 -2
- package/dist/worker/github-transfer.js +72 -2
- package/dist/worker/task-runner.js +53 -3
- package/package.json +2 -2
- package/dist/editor/fields/markdown-export-options.d.ts +0 -2
- 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.
|
|
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.
|
|
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
|
|
5
|
-
* refname and accepts a tab, so nothing here is
|
|
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
|
|
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.
|
|
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
|
|
5
|
-
* refname and accepts a tab, so nothing here is
|
|
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
|
|
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.
|
|
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 >=
|
|
171
|
-
throw new BundleHeaderError(`its header does not end within ${
|
|
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
|
|
4
|
-
* `
|
|
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
|
-
|
|
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);
|
|
@@ -631,8 +632,8 @@ const POLL_WAIT_MS = MAX_POLL_WAIT_MS;
|
|
|
631
632
|
/** How long `health` waits for the gateway: `getSecret` alone has taken 87 s. */
|
|
632
633
|
const HEALTH_WAIT_MS = 300_000;
|
|
633
634
|
/**
|
|
634
|
-
* How long `authSnapshot` waits: a
|
|
635
|
-
*
|
|
635
|
+
* How long `authSnapshot` waits: a generous bound on a fetch that pages the provider's whole user,
|
|
636
|
+
* organization and membership lists one page at a time. A later call joins a fetch an earlier one stopped waiting for.
|
|
636
637
|
*/
|
|
637
638
|
const AUTH_SNAPSHOT_WAIT_MS = 30 * 60_000;
|
|
638
639
|
/** How long `termination` and `completeLifecycle` wait: a poll's wait is well inside it. */
|
|
@@ -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
|
}>;
|