@north-light/crouter 0.3.196 → 0.3.198
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/dist/api/client.d.ts +2 -2
- package/dist/api/dto/inbox.d.ts +46 -0
- package/dist/builtin-memory/00-runtime-base.md +3 -3
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/render/chat-view.d.ts +3 -8
- package/dist/clients/attach/render/chat-view.js +3 -24
- package/dist/clients/attach/render/markdown-source.js +0 -6
- package/dist/clients/attach/render/tool-calls.js +16 -2
- package/dist/clients/attach/viewer.js +613 -602
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
- package/dist/clients/inbox/controller.js +4 -4
- package/dist/clients/inbox/page-adapter.d.ts +2 -1
- package/dist/clients/inbox/tui/render.js +46 -25
- package/dist/clients/inbox/tui/slots.d.ts +1 -3
- package/dist/clients/inbox/tui/slots.js +8 -37
- package/dist/clients/inbox/tui/types.d.ts +4 -3
- package/dist/commands/__tests__/human.test.js +160 -23
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/human/doc.js +21 -74
- package/dist/commands/human/html.d.ts +2 -0
- package/dist/commands/human/html.js +81 -0
- package/dist/commands/human/prompts.d.ts +5 -4
- package/dist/commands/human/prompts.js +90 -168
- package/dist/commands/human/shared.d.ts +5 -6
- package/dist/commands/human/shared.js +13 -61
- package/dist/commands/human.js +7 -2
- package/dist/core/__tests__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- package/dist/core/__tests__/human-deliver.test.js +28 -37
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
- package/dist/core/canvas/__tests__/attention.test.js +2 -2
- package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +26 -0
- package/dist/core/canvas/canvas.js +55 -0
- package/dist/core/command.js +6 -11
- package/dist/core/config.js +3 -2
- package/dist/core/help.d.ts +3 -0
- package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/html-markdown.test.js +52 -0
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +16 -0
- package/dist/core/human/__tests__/page-render.test.js +40 -23
- package/dist/core/human/__tests__/page-tickets.test.js +48 -27
- package/dist/core/human/__tests__/page.test.js +48 -65
- package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +51 -0
- package/dist/core/human/answer.js +125 -0
- package/dist/core/human/component-docs.d.ts +14 -0
- package/dist/core/human/component-docs.js +161 -0
- package/dist/core/human/html-markdown.d.ts +3 -0
- package/dist/core/human/html-markdown.js +288 -0
- package/dist/core/human/page-catalog.d.ts +19 -3
- package/dist/core/human/page-catalog.js +73 -16
- package/dist/core/human/page-render.d.ts +3 -2
- package/dist/core/human/page-render.js +19 -45
- package/dist/core/human/page-schema.d.ts +108 -8
- package/dist/core/human/page-schema.js +88 -31
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +23 -7
- package/dist/core/human/page.d.ts +14 -7
- package/dist/core/human/page.js +178 -90
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- package/dist/core/human/scan.d.ts +3 -2
- package/dist/core/human/scan.js +7 -8
- package/dist/core/human/summary.d.ts +4 -7
- package/dist/core/human/summary.js +4 -60
- package/dist/core/human/tickets.d.ts +10 -9
- package/dist/core/human/tickets.js +5 -3
- package/dist/core/human/types.d.ts +4 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/review/realize.js +9 -1
- package/dist/core/runtime/boot-root.js +6 -1
- package/dist/core/runtime/broker.js +8 -6
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- package/dist/core/runtime/spawn.js +4 -4
- package/dist/core/termrender/display.d.ts +7 -3
- package/dist/core/termrender/display.js +45 -9
- package/dist/core/termrender/termrender.d.ts +3 -12
- package/dist/core/termrender/termrender.js +6 -37
- package/dist/core/user-settings.d.ts +17 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/daemon/api/handlers/human.js +1 -1
- package/dist/daemon/api/handlers/inbox.js +22 -7
- package/dist/daemon/cron-run.js +16 -2
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +2 -1
- package/dist/daemon/human/finish.js +45 -28
- package/dist/daemon/human/sweep.js +4 -2
- package/dist/daemon/reconcilers/broker-supervision.js +11 -8
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
- package/dist/daemon/review/sweep.js +12 -1
- package/dist/pages/bundle.css +1 -1
- package/dist/pages/bundle.js +1008 -577
- package/dist/pages/comments.d.ts +29 -0
- package/dist/pages/comments.js +51 -0
- package/dist/pages/controls.d.ts +98 -0
- package/dist/pages/controls.js +261 -0
- package/dist/pages/elements/cards.d.ts +15 -4
- package/dist/pages/elements/cards.js +299 -186
- package/dist/pages/elements/chart.d.ts +22 -0
- package/dist/pages/elements/chart.js +107 -91
- package/dist/pages/elements/options.d.ts +9 -0
- package/dist/pages/elements/options.js +268 -155
- package/dist/pages/elements/pages.d.ts +6 -0
- package/dist/pages/elements/pages.js +60 -38
- package/dist/pages/elements/slot.d.ts +5 -0
- package/dist/pages/elements/slot.js +17 -10
- package/dist/pages/elements/table.d.ts +17 -0
- package/dist/pages/elements/table.js +336 -228
- package/dist/pages/elements/text.d.ts +9 -3
- package/dist/pages/elements/text.js +350 -143
- package/dist/pages/host.d.ts +0 -24
- package/dist/pages/host.js +0 -17
- package/dist/pages/readonly.d.ts +24 -0
- package/dist/pages/readonly.js +31 -0
- package/dist/pages/responses.d.ts +37 -0
- package/dist/pages/responses.js +56 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +12 -8
- package/dist/types.d.ts +19 -5
- package/dist/types.js +2 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/install-runtime.mjs +19 -24
- package/dist/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/core/human/markdown-html.d.ts +0 -8
- package/dist/core/human/markdown-html.js +0 -581
|
@@ -1,581 +0,0 @@
|
|
|
1
|
-
// Markdown → HTML for md-dialect page prose. Hand-rolled and dependency-free:
|
|
2
|
-
// crtr ships no markdown-to-HTML dependency, and the only markdown machinery it
|
|
3
|
-
// has (`core/termrender`) renders ANSI terminal lines through a managed Python
|
|
4
|
-
// venv — the wrong thing to invoke inside a daemon HTTP handler.
|
|
5
|
-
//
|
|
6
|
-
// The supported subset is a deliberate CommonMark slice, pinned here because
|
|
7
|
-
// page authors and page hosts both read it as the contract:
|
|
8
|
-
//
|
|
9
|
-
// Blocks — ATX headings (`#`…`######`), setext headings (`===`/`---`
|
|
10
|
-
// underlines), fenced code (``` and ~~~, info string → `class="language-x"`),
|
|
11
|
-
// blockquotes, thematic breaks, bullet lists (`-`, `*`, `+`), ordered lists
|
|
12
|
-
// (`1.`, `1)`), GFM pipe tables (with `:---:` alignment), raw HTML blocks,
|
|
13
|
-
// and paragraphs. Lists nest by indentation and render tight or loose.
|
|
14
|
-
// Inline — backslash escapes, code spans, `**strong**`, `*em*`, `***both***`
|
|
15
|
-
// (and the `_` forms, intraword-safe), links, images, autolinks
|
|
16
|
-
// (`<https://…>`, `<a@b.c>`), raw inline HTML, character entities, and
|
|
17
|
-
// two-space hard breaks.
|
|
18
|
-
//
|
|
19
|
-
// NOT supported (authors get the literal text): reference links and
|
|
20
|
-
// definitions, footnotes, indented (four-space) code blocks, task-list
|
|
21
|
-
// checkboxes, strikethrough, and HTML comments as block separators.
|
|
22
|
-
//
|
|
23
|
-
// Raw HTML passes through verbatim. A page document is authored by the agent
|
|
24
|
-
// that owns the ticket, and the html dialect is whole-document passthrough by
|
|
25
|
-
// design — md prose is not a less-trusted channel than the document carrying it.
|
|
26
|
-
const ENTITY = /^&(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#\d{1,7}|#[xX][0-9a-fA-F]{1,6});/;
|
|
27
|
-
const ASCII_PUNCTUATION = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
|
|
28
|
-
const AUTOLINK_URI = /^<([a-zA-Z][a-zA-Z0-9+.-]{1,31}:[^<>\u0000-\u0020]*)>/;
|
|
29
|
-
const AUTOLINK_EMAIL = /^<([^\s@<>]+@[^\s@<>]+\.[^\s@<>]+)>/;
|
|
30
|
-
const RAW_INLINE_TAG = /^<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^<>]*?)?\/?>/;
|
|
31
|
-
const HTML_COMMENT = /^<!--[\s\S]*?-->/;
|
|
32
|
-
const LIST_ITEM = /^( {0,3})([-*+]|\d{1,9}[.)])( +|$)(.*)$/;
|
|
33
|
-
const THEMATIC_BREAK = /^ {0,3}([-*_])(?: *\1){2,} *$/;
|
|
34
|
-
/** Escape text for an HTML text node. */
|
|
35
|
-
export function escapeHtmlText(raw) {
|
|
36
|
-
return raw.replace(/[&<>]/g, (character) => (character === '&' ? '&' : character === '<' ? '<' : '>'));
|
|
37
|
-
}
|
|
38
|
-
/** Escape a value for a double-quoted HTML attribute. */
|
|
39
|
-
export function escapeHtmlAttribute(raw) {
|
|
40
|
-
return escapeHtmlText(raw).replace(/"/g, '"');
|
|
41
|
-
}
|
|
42
|
-
/** Render one markdown document fragment as HTML. */
|
|
43
|
-
export function renderMarkdownToHtml(markdown) {
|
|
44
|
-
return renderBlocks(markdown.replace(/\r\n?/g, '\n').split('\n'), false);
|
|
45
|
-
}
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Blocks
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
/** `unwrapParagraphs` renders a tight list item: its paragraphs emit inline
|
|
50
|
-
* content with no `<p>` wrapper, exactly as CommonMark specifies. */
|
|
51
|
-
function renderBlocks(lines, unwrapParagraphs) {
|
|
52
|
-
const blocks = [];
|
|
53
|
-
let index = 0;
|
|
54
|
-
while (index < lines.length) {
|
|
55
|
-
const line = lines[index];
|
|
56
|
-
if (line.trim() === '') {
|
|
57
|
-
index++;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const fence = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
61
|
-
if (fence !== null) {
|
|
62
|
-
index = pushFencedCode(blocks, lines, index, fence[1], fence[2]);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const heading = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/.exec(line);
|
|
66
|
-
if (heading !== null) {
|
|
67
|
-
const level = heading[1].length;
|
|
68
|
-
blocks.push(`<h${level}>${renderInline((heading[2] ?? '').replace(/[ \t]+#+[ \t]*$/, ''))}</h${level}>`);
|
|
69
|
-
index++;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (THEMATIC_BREAK.test(line)) {
|
|
73
|
-
blocks.push('<hr />');
|
|
74
|
-
index++;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (/^ {0,3}>/.test(line)) {
|
|
78
|
-
index = pushBlockquote(blocks, lines, index);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (LIST_ITEM.test(line) && !THEMATIC_BREAK.test(line)) {
|
|
82
|
-
index = pushList(blocks, lines, index);
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (isHtmlBlockStart(line)) {
|
|
86
|
-
index = pushHtmlBlock(blocks, lines, index);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const tableEnd = pushTable(blocks, lines, index);
|
|
90
|
-
if (tableEnd !== null) {
|
|
91
|
-
index = tableEnd;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
index = pushParagraph(blocks, lines, index, unwrapParagraphs);
|
|
95
|
-
}
|
|
96
|
-
return blocks.join('\n');
|
|
97
|
-
}
|
|
98
|
-
function closesFence(text, opener) {
|
|
99
|
-
const trimmed = text.trim();
|
|
100
|
-
return trimmed.length >= opener.length && trimmed.split('').every((character) => character === opener[0]);
|
|
101
|
-
}
|
|
102
|
-
function pushFencedCode(blocks, lines, start, opener, info) {
|
|
103
|
-
let end = start + 1;
|
|
104
|
-
while (end < lines.length && !closesFence(lines[end], opener))
|
|
105
|
-
end++;
|
|
106
|
-
const body = lines.slice(start + 1, end);
|
|
107
|
-
const language = info.trim().split(/\s+/)[0] ?? '';
|
|
108
|
-
const attribute = language === '' ? '' : ` class="language-${escapeHtmlAttribute(language)}"`;
|
|
109
|
-
blocks.push(`<pre><code${attribute}>${escapeHtmlText(body.join('\n'))}${body.length === 0 ? '' : '\n'}</code></pre>`);
|
|
110
|
-
return end < lines.length ? end + 1 : end;
|
|
111
|
-
}
|
|
112
|
-
function pushBlockquote(blocks, lines, start) {
|
|
113
|
-
const quoted = [];
|
|
114
|
-
let index = start;
|
|
115
|
-
while (index < lines.length && /^ {0,3}>/.test(lines[index])) {
|
|
116
|
-
quoted.push(lines[index].replace(/^ {0,3}> ?/, ''));
|
|
117
|
-
index++;
|
|
118
|
-
// A lazy continuation line belongs to the quoted paragraph.
|
|
119
|
-
while (index < lines.length && lines[index].trim() !== '' && !/^ {0,3}>/.test(lines[index]) && !startsNewBlock(lines[index])) {
|
|
120
|
-
quoted.push(lines[index]);
|
|
121
|
-
index++;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
blocks.push(`<blockquote>\n${renderBlocks(quoted, false)}\n</blockquote>`);
|
|
125
|
-
return index;
|
|
126
|
-
}
|
|
127
|
-
function startsNewBlock(line) {
|
|
128
|
-
return /^ {0,3}(`{3,}|~{3,})/.test(line)
|
|
129
|
-
|| /^ {0,3}#{1,6}(\s|$)/.test(line)
|
|
130
|
-
|| THEMATIC_BREAK.test(line)
|
|
131
|
-
|| /^ {0,3}>/.test(line)
|
|
132
|
-
|| LIST_ITEM.test(line)
|
|
133
|
-
|| isHtmlBlockStart(line);
|
|
134
|
-
}
|
|
135
|
-
function isHtmlBlockStart(line) {
|
|
136
|
-
return /^ {0,3}<(?:[A-Za-z][A-Za-z0-9-]*|\/[A-Za-z][A-Za-z0-9-]*|!--)/.test(line);
|
|
137
|
-
}
|
|
138
|
-
function pushHtmlBlock(blocks, lines, start) {
|
|
139
|
-
let end = start;
|
|
140
|
-
while (end < lines.length && lines[end].trim() !== '')
|
|
141
|
-
end++;
|
|
142
|
-
blocks.push(lines.slice(start, end).join('\n'));
|
|
143
|
-
return end;
|
|
144
|
-
}
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
// Lists
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
function markerType(marker) {
|
|
149
|
-
return /^\d/.test(marker) ? 'ordered' : 'bullet';
|
|
150
|
-
}
|
|
151
|
-
function pushList(blocks, lines, start) {
|
|
152
|
-
const first = LIST_ITEM.exec(lines[start]);
|
|
153
|
-
const type = markerType(first[2]);
|
|
154
|
-
const items = [];
|
|
155
|
-
let current = null;
|
|
156
|
-
let contentIndent = 0;
|
|
157
|
-
let loose = false;
|
|
158
|
-
let pendingBlank = false;
|
|
159
|
-
let index = start;
|
|
160
|
-
while (index < lines.length) {
|
|
161
|
-
const line = lines[index];
|
|
162
|
-
if (line.trim() === '') {
|
|
163
|
-
pendingBlank = true;
|
|
164
|
-
index++;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const item = LIST_ITEM.exec(line);
|
|
168
|
-
const itemIndent = item === null ? 0 : item[1].length;
|
|
169
|
-
if (item !== null && !THEMATIC_BREAK.test(line) && markerType(item[2]) === type && (current === null || itemIndent < contentIndent)) {
|
|
170
|
-
if (current !== null && pendingBlank)
|
|
171
|
-
loose = true;
|
|
172
|
-
contentIndent = itemIndent + item[2].length + (item[3] === '' ? 1 : item[3].length);
|
|
173
|
-
current = [item[4]];
|
|
174
|
-
items.push(current);
|
|
175
|
-
pendingBlank = false;
|
|
176
|
-
index++;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (current === null)
|
|
180
|
-
break;
|
|
181
|
-
const indented = /^ +/.exec(line)?.[0]?.length ?? 0;
|
|
182
|
-
if (indented >= contentIndent) {
|
|
183
|
-
if (pendingBlank) {
|
|
184
|
-
current.push('');
|
|
185
|
-
loose = true;
|
|
186
|
-
}
|
|
187
|
-
current.push(line.slice(contentIndent));
|
|
188
|
-
pendingBlank = false;
|
|
189
|
-
index++;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
// A lazy continuation of the item's paragraph — but only before a blank line.
|
|
193
|
-
if (!pendingBlank && !startsNewBlock(line)) {
|
|
194
|
-
current.push(line);
|
|
195
|
-
index++;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
const rendered = items.map((item) => `<li>${wrapListItem(renderBlocks(item, !loose))}</li>`);
|
|
201
|
-
const tag = type === 'ordered' ? 'ol' : 'ul';
|
|
202
|
-
const startNumber = type === 'ordered' ? Number.parseInt(first[2], 10) : 1;
|
|
203
|
-
const startAttribute = type === 'ordered' && startNumber !== 1 ? ` start="${startNumber}"` : '';
|
|
204
|
-
blocks.push(`<${tag}${startAttribute}>\n${rendered.join('\n')}\n</${tag}>`);
|
|
205
|
-
return index;
|
|
206
|
-
}
|
|
207
|
-
function wrapListItem(html) {
|
|
208
|
-
return html.includes('\n') ? `\n${html}\n` : html;
|
|
209
|
-
}
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
// GFM tables
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
function splitRow(line) {
|
|
214
|
-
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
215
|
-
const cells = [];
|
|
216
|
-
let cell = '';
|
|
217
|
-
for (let index = 0; index < trimmed.length; index++) {
|
|
218
|
-
const character = trimmed[index];
|
|
219
|
-
if (character === '\\' && trimmed[index + 1] === '|') {
|
|
220
|
-
cell += '|';
|
|
221
|
-
index++;
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (character === '|') {
|
|
225
|
-
cells.push(cell.trim());
|
|
226
|
-
cell = '';
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
cell += character;
|
|
230
|
-
}
|
|
231
|
-
cells.push(cell.trim());
|
|
232
|
-
return cells;
|
|
233
|
-
}
|
|
234
|
-
/** Render a GFM pipe table at `start`, or return null when there is none. */
|
|
235
|
-
function pushTable(blocks, lines, start) {
|
|
236
|
-
const header = lines[start];
|
|
237
|
-
const delimiter = lines[start + 1];
|
|
238
|
-
if (!header.includes('|') || delimiter === undefined)
|
|
239
|
-
return null;
|
|
240
|
-
if (!/^ {0,3}\|?(?: *:?-+:? *\|)+ *:?-*:? *\|? *$/.test(delimiter) && !/^ {0,3}:?-+:? *$/.test(delimiter))
|
|
241
|
-
return null;
|
|
242
|
-
const alignments = splitRow(delimiter).map((cell) => {
|
|
243
|
-
if (!/^:?-+:?$/.test(cell))
|
|
244
|
-
return null;
|
|
245
|
-
if (cell.startsWith(':') && cell.endsWith(':'))
|
|
246
|
-
return 'center';
|
|
247
|
-
if (cell.endsWith(':'))
|
|
248
|
-
return 'right';
|
|
249
|
-
if (cell.startsWith(':'))
|
|
250
|
-
return 'left';
|
|
251
|
-
return '';
|
|
252
|
-
});
|
|
253
|
-
if (alignments.some((alignment) => alignment === null))
|
|
254
|
-
return null;
|
|
255
|
-
const headerCells = splitRow(header);
|
|
256
|
-
if (headerCells.length !== alignments.length)
|
|
257
|
-
return null;
|
|
258
|
-
const align = (column) => {
|
|
259
|
-
const alignment = alignments[column];
|
|
260
|
-
return alignment === undefined || alignment === '' ? '' : ` style="text-align:${alignment}"`;
|
|
261
|
-
};
|
|
262
|
-
const rows = [];
|
|
263
|
-
let index = start + 2;
|
|
264
|
-
while (index < lines.length && lines[index].trim() !== '' && lines[index].includes('|') && !startsNewBlock(lines[index])) {
|
|
265
|
-
const cells = splitRow(lines[index]);
|
|
266
|
-
const padded = alignments.map((_, column) => cells[column] ?? '');
|
|
267
|
-
rows.push(`<tr>${padded.map((cell, column) => `<td${align(column)}>${renderInline(cell)}</td>`).join('')}</tr>`);
|
|
268
|
-
index++;
|
|
269
|
-
}
|
|
270
|
-
const head = `<thead>\n<tr>${headerCells.map((cell, column) => `<th${align(column)}>${renderInline(cell)}</th>`).join('')}</tr>\n</thead>`;
|
|
271
|
-
const body = rows.length === 0 ? '' : `\n<tbody>\n${rows.join('\n')}\n</tbody>`;
|
|
272
|
-
blocks.push(`<table>\n${head}${body}\n</table>`);
|
|
273
|
-
return index;
|
|
274
|
-
}
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
// Paragraphs
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
|
-
function pushParagraph(blocks, lines, start, unwrap) {
|
|
279
|
-
const collected = [lines[start].trim()];
|
|
280
|
-
let index = start + 1;
|
|
281
|
-
while (index < lines.length) {
|
|
282
|
-
const line = lines[index];
|
|
283
|
-
if (line.trim() === '')
|
|
284
|
-
break;
|
|
285
|
-
if (/^ {0,3}=+ *$/.test(line)) {
|
|
286
|
-
blocks.push(`<h1>${renderInline(collected.join('\n'))}</h1>`);
|
|
287
|
-
return index + 1;
|
|
288
|
-
}
|
|
289
|
-
if (/^ {0,3}-+ *$/.test(line)) {
|
|
290
|
-
blocks.push(`<h2>${renderInline(collected.join('\n'))}</h2>`);
|
|
291
|
-
return index + 1;
|
|
292
|
-
}
|
|
293
|
-
if (startsNewBlock(line))
|
|
294
|
-
break;
|
|
295
|
-
collected.push(line.replace(/^ {0,3}/, ''));
|
|
296
|
-
index++;
|
|
297
|
-
}
|
|
298
|
-
const inline = renderInline(collected.join('\n'));
|
|
299
|
-
blocks.push(unwrap ? inline : `<p>${inline}</p>`);
|
|
300
|
-
return index;
|
|
301
|
-
}
|
|
302
|
-
// ---------------------------------------------------------------------------
|
|
303
|
-
// Inline
|
|
304
|
-
// ---------------------------------------------------------------------------
|
|
305
|
-
/** Render inline markdown. Exported for callers rendering a single line. */
|
|
306
|
-
export function renderInline(source) {
|
|
307
|
-
let out = '';
|
|
308
|
-
let index = 0;
|
|
309
|
-
while (index < source.length) {
|
|
310
|
-
const character = source[index];
|
|
311
|
-
if (character === '\\') {
|
|
312
|
-
const next = source[index + 1];
|
|
313
|
-
if (next === '\n') {
|
|
314
|
-
out += '<br />\n';
|
|
315
|
-
index += 2;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (next !== undefined && ASCII_PUNCTUATION.test(next)) {
|
|
319
|
-
out += escapeHtmlText(next);
|
|
320
|
-
index += 2;
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
out += '\\';
|
|
324
|
-
index++;
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (character === ' ') {
|
|
328
|
-
const run = /^( +)(\n|$)/.exec(source.slice(index));
|
|
329
|
-
if (run !== null) {
|
|
330
|
-
out += run[1].length >= 2 && run[2] === '\n' ? '<br />\n' : run[2] === '\n' ? '\n' : '';
|
|
331
|
-
index += run[1].length + run[2].length;
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
out += ' ';
|
|
335
|
-
index++;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (character === '`') {
|
|
339
|
-
const span = codeSpanAt(source, index);
|
|
340
|
-
if (span !== null) {
|
|
341
|
-
out += span.html;
|
|
342
|
-
index = span.end;
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
out += '`';
|
|
346
|
-
index++;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
if (character === '<') {
|
|
350
|
-
const raw = rawInlineAt(source, index);
|
|
351
|
-
if (raw !== null) {
|
|
352
|
-
out += raw.html;
|
|
353
|
-
index = raw.end;
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
out += '<';
|
|
357
|
-
index++;
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
if (character === '&') {
|
|
361
|
-
const entity = ENTITY.exec(source.slice(index));
|
|
362
|
-
if (entity !== null) {
|
|
363
|
-
out += entity[0];
|
|
364
|
-
index += entity[0].length;
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
out += '&';
|
|
368
|
-
index++;
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
if (character === '>') {
|
|
372
|
-
out += '>';
|
|
373
|
-
index++;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (character === '!' && source[index + 1] === '[') {
|
|
377
|
-
const image = linkAt(source, index + 1);
|
|
378
|
-
if (image !== null) {
|
|
379
|
-
out += `<img src="${escapeHtmlAttribute(image.destination)}" alt="${escapeHtmlAttribute(stripInlineMarkup(image.text))}"${image.title === undefined ? '' : ` title="${escapeHtmlAttribute(image.title)}"`} />`;
|
|
380
|
-
index = image.end;
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (character === '[') {
|
|
385
|
-
const link = linkAt(source, index);
|
|
386
|
-
if (link !== null) {
|
|
387
|
-
out += `<a href="${escapeHtmlAttribute(link.destination)}"${link.title === undefined ? '' : ` title="${escapeHtmlAttribute(link.title)}"`}>${renderInline(link.text)}</a>`;
|
|
388
|
-
index = link.end;
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (character === '*' || character === '_') {
|
|
393
|
-
const emphasis = emphasisAt(source, index);
|
|
394
|
-
if (emphasis !== null) {
|
|
395
|
-
out += emphasis.html;
|
|
396
|
-
index = emphasis.end;
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
out += character;
|
|
400
|
-
index++;
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
out += character;
|
|
404
|
-
index++;
|
|
405
|
-
}
|
|
406
|
-
return out;
|
|
407
|
-
}
|
|
408
|
-
/** Plain text for an image `alt`, which carries no markup. */
|
|
409
|
-
function stripInlineMarkup(source) {
|
|
410
|
-
return source.replace(/[*_`]/g, '');
|
|
411
|
-
}
|
|
412
|
-
function backtickRun(source, at) {
|
|
413
|
-
let run = 0;
|
|
414
|
-
while (source[at + run] === '`')
|
|
415
|
-
run++;
|
|
416
|
-
return run;
|
|
417
|
-
}
|
|
418
|
-
function codeSpanAt(source, start) {
|
|
419
|
-
const run = backtickRun(source, start);
|
|
420
|
-
let index = start + run;
|
|
421
|
-
while (index < source.length) {
|
|
422
|
-
if (source[index] !== '`') {
|
|
423
|
-
index++;
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
const closing = backtickRun(source, index);
|
|
427
|
-
if (closing === run) {
|
|
428
|
-
let content = source.slice(start + run, index).replace(/\n/g, ' ');
|
|
429
|
-
if (content.length > 2 && content.startsWith(' ') && content.endsWith(' ') && content.trim() !== '')
|
|
430
|
-
content = content.slice(1, -1);
|
|
431
|
-
return { html: `<code>${escapeHtmlText(content)}</code>`, end: index + closing };
|
|
432
|
-
}
|
|
433
|
-
index += closing;
|
|
434
|
-
}
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
function rawInlineAt(source, start) {
|
|
438
|
-
const rest = source.slice(start);
|
|
439
|
-
const uri = AUTOLINK_URI.exec(rest);
|
|
440
|
-
if (uri !== null)
|
|
441
|
-
return { html: `<a href="${escapeHtmlAttribute(uri[1])}">${escapeHtmlText(uri[1])}</a>`, end: start + uri[0].length };
|
|
442
|
-
const email = AUTOLINK_EMAIL.exec(rest);
|
|
443
|
-
if (email !== null)
|
|
444
|
-
return { html: `<a href="mailto:${escapeHtmlAttribute(email[1])}">${escapeHtmlText(email[1])}</a>`, end: start + email[0].length };
|
|
445
|
-
const comment = HTML_COMMENT.exec(rest);
|
|
446
|
-
if (comment !== null)
|
|
447
|
-
return { html: comment[0], end: start + comment[0].length };
|
|
448
|
-
const tag = RAW_INLINE_TAG.exec(rest);
|
|
449
|
-
if (tag !== null)
|
|
450
|
-
return { html: tag[0], end: start + tag[0].length };
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
|
-
function linkAt(source, start) {
|
|
454
|
-
let depth = 0;
|
|
455
|
-
let index = start;
|
|
456
|
-
let textEnd = -1;
|
|
457
|
-
while (index < source.length) {
|
|
458
|
-
const character = source[index];
|
|
459
|
-
if (character === '\\') {
|
|
460
|
-
index += 2;
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
if (character === '`') {
|
|
464
|
-
const span = codeSpanAt(source, index);
|
|
465
|
-
index = span === null ? index + 1 : span.end;
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
if (character === '[')
|
|
469
|
-
depth++;
|
|
470
|
-
else if (character === ']') {
|
|
471
|
-
depth--;
|
|
472
|
-
if (depth === 0) {
|
|
473
|
-
textEnd = index;
|
|
474
|
-
break;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
index++;
|
|
478
|
-
}
|
|
479
|
-
if (textEnd === -1 || source[textEnd + 1] !== '(')
|
|
480
|
-
return null;
|
|
481
|
-
let cursor = textEnd + 2;
|
|
482
|
-
while (source[cursor] === ' ' || source[cursor] === '\n')
|
|
483
|
-
cursor++;
|
|
484
|
-
let destination = '';
|
|
485
|
-
if (source[cursor] === '<') {
|
|
486
|
-
const close = source.indexOf('>', cursor + 1);
|
|
487
|
-
if (close === -1)
|
|
488
|
-
return null;
|
|
489
|
-
destination = source.slice(cursor + 1, close);
|
|
490
|
-
cursor = close + 1;
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
let parens = 0;
|
|
494
|
-
while (cursor < source.length) {
|
|
495
|
-
const character = source[cursor];
|
|
496
|
-
if (character === '\\' && source[cursor + 1] !== undefined) {
|
|
497
|
-
destination += source[cursor + 1];
|
|
498
|
-
cursor += 2;
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
if (/\s/.test(character))
|
|
502
|
-
break;
|
|
503
|
-
if (character === '(')
|
|
504
|
-
parens++;
|
|
505
|
-
if (character === ')') {
|
|
506
|
-
if (parens === 0)
|
|
507
|
-
break;
|
|
508
|
-
parens--;
|
|
509
|
-
}
|
|
510
|
-
destination += character;
|
|
511
|
-
cursor++;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
while (source[cursor] === ' ' || source[cursor] === '\n')
|
|
515
|
-
cursor++;
|
|
516
|
-
let title;
|
|
517
|
-
const quote = source[cursor];
|
|
518
|
-
if (quote === '"' || quote === "'") {
|
|
519
|
-
const close = source.indexOf(quote, cursor + 1);
|
|
520
|
-
if (close === -1)
|
|
521
|
-
return null;
|
|
522
|
-
title = source.slice(cursor + 1, close);
|
|
523
|
-
cursor = close + 1;
|
|
524
|
-
while (source[cursor] === ' ' || source[cursor] === '\n')
|
|
525
|
-
cursor++;
|
|
526
|
-
}
|
|
527
|
-
if (source[cursor] !== ')')
|
|
528
|
-
return null;
|
|
529
|
-
return { text: source.slice(start + 1, textEnd), destination, ...(title === undefined ? {} : { title }), end: cursor + 1 };
|
|
530
|
-
}
|
|
531
|
-
function emphasisAt(source, start) {
|
|
532
|
-
const marker = source[start];
|
|
533
|
-
let run = 0;
|
|
534
|
-
while (source[start + run] === marker)
|
|
535
|
-
run++;
|
|
536
|
-
const need = Math.min(run, 3);
|
|
537
|
-
const opened = source[start + need];
|
|
538
|
-
if (opened === undefined || /\s/.test(opened))
|
|
539
|
-
return null;
|
|
540
|
-
if (marker === '_') {
|
|
541
|
-
const before = source[start - 1];
|
|
542
|
-
if (before !== undefined && /[A-Za-z0-9]/.test(before))
|
|
543
|
-
return null;
|
|
544
|
-
}
|
|
545
|
-
const closer = findEmphasisCloser(source, start + need, marker, need);
|
|
546
|
-
if (closer === null)
|
|
547
|
-
return null;
|
|
548
|
-
const inner = renderInline(source.slice(start + need, closer));
|
|
549
|
-
const html = need === 1 ? `<em>${inner}</em>` : need === 2 ? `<strong>${inner}</strong>` : `<strong><em>${inner}</em></strong>`;
|
|
550
|
-
return { html, end: closer + need };
|
|
551
|
-
}
|
|
552
|
-
function findEmphasisCloser(source, from, marker, need) {
|
|
553
|
-
let index = from;
|
|
554
|
-
while (index < source.length) {
|
|
555
|
-
const character = source[index];
|
|
556
|
-
if (character === '\\') {
|
|
557
|
-
index += 2;
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
if (character === '`') {
|
|
561
|
-
const span = codeSpanAt(source, index);
|
|
562
|
-
index = span === null ? index + 1 : span.end;
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
if (character !== marker) {
|
|
566
|
-
index++;
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
let run = 0;
|
|
570
|
-
while (source[index + run] === marker)
|
|
571
|
-
run++;
|
|
572
|
-
const before = source[index - 1];
|
|
573
|
-
const after = source[index + run];
|
|
574
|
-
const closes = run >= need && before !== undefined && !/\s/.test(before)
|
|
575
|
-
&& !(marker === '_' && after !== undefined && /[A-Za-z0-9]/.test(after));
|
|
576
|
-
if (closes && index > from)
|
|
577
|
-
return index;
|
|
578
|
-
index += run;
|
|
579
|
-
}
|
|
580
|
-
return null;
|
|
581
|
-
}
|