@magland/mochi 0.3.1

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 (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. package/package.json +71 -0
package/dist/logo.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ // The Mochi Forge logo.
3
+ //
4
+ // The mark is a strike at the anvil: the workpiece square resting on the
5
+ // anvil's face, with three sparks flying off the point of the blow. The
6
+ // logotype sets the short name in letters built from the same two parts, a
7
+ // monoline stroke and a piece of a circle, so mark and word read as one
8
+ // drawing. The letters are strokes rather than outlines, so there is no font
9
+ // dependency and no text-to-path step.
10
+ //
11
+ // The mark sits on a 64 x 64 box, stroke 6, round caps. The workpiece is a
12
+ // 14 x 14 square with corner radius 3, resting half a stroke above the anvil
13
+ // face, a 36-unit horizontal at the bottom. The sparks are stroke segments
14
+ // radiating from the square's top centre at -140, -90, and -40 degrees, from
15
+ // radius 11 out to radius 19, so they cannot help but point back at where the
16
+ // hammer lands. The square's height is solved so the ink margins above and
17
+ // below come out equal: ink runs 9.5 to 54.5 vertically and 11 to 53 across
18
+ // the anvil, which centres the glyph with about 10 units of clear space.
19
+ //
20
+ // The logotype sits on a 232 x 60 box: ascender at 0, x-height top at 20,
21
+ // baseline at 60, stroke 10, round caps and joins. Radius 15 is the only
22
+ // curve in the word, centred on y = 40 throughout: o is a full circle of it,
23
+ // c is the same circle cut open 50 degrees either side of due east, and the
24
+ // arches of m and h are its top half, springing from one stem and landing on
25
+ // the next. Letters carry 8 units of air between outer edges. h's stem and
26
+ // i's tittle are the only ink above the x-height, and no letter descends, so
27
+ // the box stops half a stroke below the baseline. Scaling is uniform only,
28
+ // since the stroke weight is constant by construction and stretching one
29
+ // axis would break it.
30
+ //
31
+ // Both carry stroke="currentColor" so they take the surrounding text colour
32
+ // and need no per-theme variants. The favicon is the exception: nothing there
33
+ // inherits a colour, so it is filled from the active theme (see faviconSvg).
34
+ //
35
+ // The same drawings are checked in as standalone files under logo/assets, for
36
+ // use outside the server; they are inline here because the server ships no static
37
+ // files. Both come from scripts/make-logo.sh, which computes the coordinates
38
+ // from the construction rather than taking them by hand, and which fails if
39
+ // the geometry below has drifted from the files there. Edit the script.
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.MARK = exports.WORDMARK = void 0;
42
+ exports.faviconSvg = faviconSvg;
43
+ const themes_1 = require("./themes");
44
+ /** The logotype: five letters as monoline strokes and radius-15 curves. 232 x 60. */
45
+ exports.WORDMARK = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232 60" fill="none" role="img" aria-label="mochi"><g stroke="currentColor" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"><path d="M5 25V55"/><path d="M5 40A15 15 0 0 1 35 40"/><path d="M35 40V55"/><path d="M35 40A15 15 0 0 1 65 40"/><path d="M65 40V55"/><circle cx="98" cy="40" r="15"/><path d="M155.64 28.51A15 15 0 1 0 155.64 51.49"/><path d="M179 5V55"/><path d="M179 40A15 15 0 0 1 209 40"/><path d="M209 40V55"/><path d="M227 25V55"/><circle cx="227" cy="5" r="5" stroke="none" fill="currentColor"/></g></svg>`;
46
+ /** The mark: three sparks over the workpiece square on the anvil face. 64 x 64. */
47
+ exports.MARK = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" role="img" aria-label="mochi"><path d="M23.57 24.43L17.45 19.29" stroke="currentColor" stroke-width="6" stroke-linecap="round"/><path d="M32 20.5L32 12.5" stroke="currentColor" stroke-width="6" stroke-linecap="round"/><path d="M40.43 24.43L46.55 19.29" stroke="currentColor" stroke-width="6" stroke-linecap="round"/><rect x="25" y="31.5" width="14" height="14" rx="3" fill="currentColor"/><path d="M14 51.5H50" stroke="currentColor" stroke-width="6" stroke-linecap="round"/></svg>`;
48
+ /**
49
+ * The mark on a rounded tile, for the favicon and any other icon slot. This
50
+ * is logo/assets/mochi-icon.svg with its two fixed colours replaced: a
51
+ * favicon inherits nothing from the page, so they are taken from the vault's
52
+ * theme instead, the tile in the accent and the glyph in the page background.
53
+ * Every shipped theme pairs those with enough contrast. The glyph is scaled to
54
+ * 0.7 about the tile's centre, which is what holds its shape down at favicon
55
+ * sizes, where the sparks close up against the square.
56
+ */
57
+ function faviconSvg(theme = (0, themes_1.activeTheme)()) {
58
+ const bg = theme.vars.accent;
59
+ const fg = theme.vars.bg;
60
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="mochi"><rect width="64" height="64" rx="14" fill="${bg}"/><g transform="translate(9.6 9.6) scale(0.7)" fill="none"><path d="M23.57 24.43L17.45 19.29" stroke="${fg}" stroke-width="6" stroke-linecap="round"/><path d="M32 20.5L32 12.5" stroke="${fg}" stroke-width="6" stroke-linecap="round"/><path d="M40.43 24.43L46.55 19.29" stroke="${fg}" stroke-width="6" stroke-linecap="round"/><rect x="25" y="31.5" width="14" height="14" rx="3" fill="${fg}"/><path d="M14 51.5H50" stroke="${fg}" stroke-width="6" stroke-linecap="round"/></g></svg>`;
61
+ }
@@ -0,0 +1,382 @@
1
+ "use strict";
2
+ // Markdown rendering for repository documents.
3
+ //
4
+ // The target is what GitHub renders, since that is what people write their
5
+ // READMEs against: fenced code with syntax highlighting, LaTeX math, tables,
6
+ // task lists, footnotes, alert callouts, emoji shortcodes, autolinks, heading
7
+ // anchors, and a conservative subset of inline HTML.
8
+ //
9
+ // Two properties are load-bearing and should survive any later rewrite. First,
10
+ // nothing a document author writes may become active markup on this origin: the
11
+ // rendered output passes through an allowlist sanitizer, and the allowlist has
12
+ // no `style` attribute, no scripts, no frames. Second, everything is rendered
13
+ // on the server; no page needs client-side JavaScript to read.
14
+ //
15
+ // Trusted fragments we generate ourselves (highlighted code, KaTeX output)
16
+ // would not survive that allowlist intact, so they are parked in slots: the
17
+ // renderer emits an opaque marker, the sanitizer sees plain text, and the
18
+ // fragments are substituted back afterwards. The marker is random per render,
19
+ // so a document cannot forge one.
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ var __importDefault = (this && this.__importDefault) || function (mod) {
54
+ return (mod && mod.__esModule) ? mod : { "default": mod };
55
+ };
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.isMarkdownFile = isMarkdownFile;
58
+ exports.renderMarkdown = renderMarkdown;
59
+ const crypto = __importStar(require("crypto"));
60
+ const highlight_js_1 = __importDefault(require("highlight.js"));
61
+ const katex_1 = __importDefault(require("katex"));
62
+ const markdown_it_1 = __importDefault(require("markdown-it"));
63
+ const markdown_it_katex_1 = __importDefault(require("@vscode/markdown-it-katex"));
64
+ const markdown_it_emoji_1 = require("markdown-it-emoji");
65
+ const markdown_it_footnote_1 = __importDefault(require("markdown-it-footnote"));
66
+ const sanitize_html_1 = __importDefault(require("sanitize-html"));
67
+ const render_1 = require("./render");
68
+ const views_1 = require("./views");
69
+ // GitHub's cross-references: `#12` is that issue, and a hex string of seven
70
+ // or more characters is that commit. The pattern refuses a run that is part
71
+ // of a longer word, and a commit id must contain at least one letter, so that
72
+ // a plain number written in passing is not mistaken for one.
73
+ const CROSS_REF = /(?<![\w#/-])(?:#(\d{1,9})|([0-9a-f]{7,40}))(?![\w-])/g;
74
+ function isMarkdownFile(filename) {
75
+ const base = filename.split('/').pop() ?? filename;
76
+ return /\.(md|markdown)$/i.test(base);
77
+ }
78
+ const ALERTS = {
79
+ note: 'Note',
80
+ tip: 'Tip',
81
+ important: 'Important',
82
+ warning: 'Warning',
83
+ caution: 'Caution',
84
+ };
85
+ // GitHub-style heading anchors: lowercase, punctuation dropped, spaces to hyphens.
86
+ function slugify(text) {
87
+ return text
88
+ .trim()
89
+ .toLowerCase()
90
+ .replace(/[^\p{L}\p{N} _-]/gu, '')
91
+ .replace(/[\s_]+/g, '-');
92
+ }
93
+ function isRelative(url) {
94
+ return !/^([a-z][a-z0-9+.-]*:|\/|#)/i.test(url);
95
+ }
96
+ // Resolve a relative reference against the file's directory, collapsing the
97
+ // dot segments ourselves so the emitted URL is the path a reader would type.
98
+ function resolveRef(url, base) {
99
+ const cut = url.search(/[#?]/);
100
+ const rel = cut === -1 ? url : url.slice(0, cut);
101
+ const suffix = cut === -1 ? '' : url.slice(cut);
102
+ const out = [];
103
+ for (const seg of `${base}/${rel}`.split('/')) {
104
+ if (seg === '.')
105
+ continue;
106
+ if (seg === '..') {
107
+ if (out.length > 1)
108
+ out.pop();
109
+ continue;
110
+ }
111
+ out.push(seg);
112
+ }
113
+ return out.join('/') + suffix;
114
+ }
115
+ const ALLOWED_TAGS = [
116
+ 'p', 'br', 'hr', 'div', 'span', 'section',
117
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
118
+ 'ul', 'ol', 'li', 'dl', 'dt', 'dd',
119
+ 'blockquote', 'pre', 'code', 'samp', 'kbd', 'var',
120
+ 'em', 'strong', 'b', 'i', 'u', 's', 'del', 'ins', 'mark', 'small', 'sub', 'sup',
121
+ 'a', 'img', 'picture',
122
+ 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',
123
+ 'details', 'summary', 'figure', 'figcaption',
124
+ 'abbr', 'cite', 'q', 'input',
125
+ ];
126
+ // No `style` anywhere: an inline style on document content could cover the
127
+ // rest of the page, which is a phishing surface even without scripting.
128
+ const ALLOWED_ATTRS = {
129
+ '*': ['id', 'class', 'title', 'align', 'dir', 'lang', 'role', 'aria-label', 'aria-hidden'],
130
+ a: ['href', 'name', 'target', 'rel'],
131
+ img: ['src', 'alt', 'width', 'height', 'loading'],
132
+ th: ['colspan', 'rowspan', 'scope', 'valign'],
133
+ td: ['colspan', 'rowspan', 'valign'],
134
+ col: ['span', 'width'],
135
+ colgroup: ['span'],
136
+ ol: ['start', 'type', 'reversed'],
137
+ li: ['value'],
138
+ details: ['open'],
139
+ input: ['type', 'checked', 'disabled'],
140
+ };
141
+ function sanitizeOptions(opts) {
142
+ return {
143
+ allowedTags: ALLOWED_TAGS,
144
+ allowedAttributes: ALLOWED_ATTRS,
145
+ allowedSchemes: ['http', 'https', 'mailto', 'ftp'],
146
+ allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
147
+ allowProtocolRelative: false,
148
+ transformTags: {
149
+ // Rewriting here rather than in a renderer rule covers links written as
150
+ // markdown and links written as HTML with one implementation.
151
+ a: (tagName, attribs) => {
152
+ const href = attribs.href;
153
+ if (href && isRelative(href)) {
154
+ attribs.href = resolveRef(href, opts.blobBase);
155
+ }
156
+ else if (href && /^https?:/i.test(href)) {
157
+ attribs.rel = 'nofollow noopener noreferrer';
158
+ }
159
+ return { tagName, attribs };
160
+ },
161
+ img: (tagName, attribs) => {
162
+ const src = attribs.src;
163
+ if (src && isRelative(src))
164
+ attribs.src = resolveRef(src, opts.rawBase);
165
+ attribs.loading = 'lazy';
166
+ return { tagName, attribs };
167
+ },
168
+ },
169
+ };
170
+ }
171
+ function highlight(code, lang) {
172
+ if (lang && highlight_js_1.default.getLanguage(lang)) {
173
+ try {
174
+ return highlight_js_1.default.highlight(code, { language: lang, ignoreIllegals: true }).value;
175
+ }
176
+ catch {
177
+ // fall through to plain escaping
178
+ }
179
+ }
180
+ return (0, render_1.esc)(code);
181
+ }
182
+ function mathHtml(src, display) {
183
+ try {
184
+ return katex_1.default.renderToString(src, {
185
+ displayMode: display,
186
+ throwOnError: false,
187
+ // trust:false keeps \href, \htmlClass and friends from emitting markup;
188
+ // maxSize bounds \rule and similar from blowing up the page.
189
+ trust: false,
190
+ maxSize: 500,
191
+ strict: false,
192
+ });
193
+ }
194
+ catch {
195
+ return `<code class="math-error">${(0, render_1.esc)(src)}</code>`;
196
+ }
197
+ }
198
+ function slot(env, html) {
199
+ env.slots.push(html);
200
+ return `${env.marker}_${env.slots.length - 1}_`;
201
+ }
202
+ function codeBlock(env, code, lang) {
203
+ const cls = lang ? ` class="language-${(0, render_1.esc)(lang)}"` : '';
204
+ // The button sits after the <pre> because the page's copyCmd() copies its
205
+ // previous sibling.
206
+ return `${slot(env, `<div class="code-block"><pre><code${cls}>${highlight(code, lang)}</code></pre>${(0, views_1.copyButton)()}</div>`)}\n`;
207
+ }
208
+ function mathBlock(env, src) {
209
+ return `<div class="math-block">${slot(env, mathHtml(src, true))}</div>\n`;
210
+ }
211
+ function buildMarkdownIt() {
212
+ const md = new markdown_it_1.default({ html: true, linkify: true });
213
+ md.use(markdown_it_footnote_1.default);
214
+ md.use(markdown_it_emoji_1.full);
215
+ // Cross-references are resolved over the inline token stream in a core
216
+ // rule, not in an inline rule: by the time inline rules run, markdown-it's
217
+ // text rule has already swallowed these characters, which is the same
218
+ // reason its own linkify works this way. Text inside a link is left alone,
219
+ // so a reference written inside one does not try to nest a second.
220
+ md.core.ruler.push('mochi_refs', (state) => {
221
+ const opts = state.env.opts;
222
+ if (!opts.issueBase && !opts.commitBase)
223
+ return;
224
+ for (const block of state.tokens) {
225
+ if (block.type !== 'inline' || !block.children)
226
+ continue;
227
+ const out = [];
228
+ let inLink = 0;
229
+ let changed = false;
230
+ for (const token of block.children) {
231
+ if (token.type === 'link_open')
232
+ inLink++;
233
+ else if (token.type === 'link_close')
234
+ inLink--;
235
+ if (token.type !== 'text' || inLink > 0) {
236
+ out.push(token);
237
+ continue;
238
+ }
239
+ let last = 0;
240
+ CROSS_REF.lastIndex = 0;
241
+ for (let m = CROSS_REF.exec(token.content); m; m = CROSS_REF.exec(token.content)) {
242
+ const [whole, issue, sha] = m;
243
+ const href = issue
244
+ ? opts.issueBase && `${opts.issueBase}/${issue}`
245
+ : sha && /[a-f]/.test(sha) && opts.commitBase
246
+ ? `${opts.commitBase}/${sha}`
247
+ : undefined;
248
+ if (!href)
249
+ continue;
250
+ if (m.index > last) {
251
+ const before = new state.Token('text', '', 0);
252
+ before.content = token.content.slice(last, m.index);
253
+ out.push(before);
254
+ }
255
+ const open = new state.Token('link_open', 'a', 1);
256
+ open.attrSet('href', href);
257
+ const label = new state.Token('text', '', 0);
258
+ // A full commit id is shown abbreviated, the way it is written
259
+ // everywhere else in the interface.
260
+ label.content = sha && sha.length > 7 ? sha.slice(0, 7) : whole;
261
+ const close = new state.Token('link_close', 'a', -1);
262
+ out.push(open, label, close);
263
+ last = m.index + whole.length;
264
+ changed = true;
265
+ }
266
+ if (last === 0) {
267
+ out.push(token);
268
+ }
269
+ else if (last < token.content.length) {
270
+ const rest = new state.Token('text', '', 0);
271
+ rest.content = token.content.slice(last);
272
+ out.push(rest);
273
+ }
274
+ }
275
+ if (changed)
276
+ block.children = out;
277
+ }
278
+ });
279
+ md.renderer.rules.fence = (tokens, idx, _options, env) => {
280
+ const token = tokens[idx];
281
+ const info = token.info.trim().split(/\s+/)[0].toLowerCase();
282
+ if (info === 'math')
283
+ return mathBlock(env, token.content);
284
+ return codeBlock(env, token.content, info || null);
285
+ };
286
+ md.renderer.rules.code_block = (tokens, idx, _options, env) => codeBlock(env, tokens[idx].content, null);
287
+ // The plugin renders math itself; we route it through a slot instead so the
288
+ // sanitizer never has to allow KaTeX's own markup.
289
+ md.use(markdown_it_katex_1.default, {});
290
+ md.renderer.rules.math_inline = (tokens, idx, _options, env) => slot(env, mathHtml(tokens[idx].content, false));
291
+ md.renderer.rules.math_block = (tokens, idx, _options, env) => mathBlock(env, tokens[idx].content);
292
+ md.renderer.rules.math_inline_block = (tokens, idx, _options, env) => mathBlock(env, tokens[idx].content);
293
+ // markdown-it expresses column alignment as an inline style, which the
294
+ // sanitizer strips; carry it in the align attribute instead, as GitHub does.
295
+ const alignRule = (tokens, idx, options, _env, self) => {
296
+ const token = tokens[idx];
297
+ const style = token.attrGet('style');
298
+ const m = style ? /text-align:\s*(left|center|right)/.exec(style) : null;
299
+ if (m && token.attrs) {
300
+ token.attrs = token.attrs.filter((a) => a[0] !== 'style');
301
+ token.attrSet('align', m[1]);
302
+ }
303
+ return self.renderToken(tokens, idx, options);
304
+ };
305
+ md.renderer.rules.th_open = alignRule;
306
+ md.renderer.rules.td_open = alignRule;
307
+ md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
308
+ const e = env;
309
+ const inline = tokens[idx + 1];
310
+ const slug = slugify(inline && inline.type === 'inline' ? inline.content : '');
311
+ e.pendingSlug = '';
312
+ if (slug) {
313
+ const seen = e.used.get(slug) ?? 0;
314
+ e.used.set(slug, seen + 1);
315
+ e.pendingSlug = seen === 0 ? slug : `${slug}-${seen}`;
316
+ tokens[idx].attrSet('id', e.pendingSlug);
317
+ }
318
+ return self.renderToken(tokens, idx, options);
319
+ };
320
+ md.renderer.rules.heading_close = (tokens, idx, options, env, self) => {
321
+ const e = env;
322
+ const anchor = e.pendingSlug
323
+ ? `<a class="heading-anchor" href="#${e.pendingSlug}" aria-label="Permalink to this heading">#</a>`
324
+ : '';
325
+ e.pendingSlug = '';
326
+ return anchor + self.renderToken(tokens, idx, options);
327
+ };
328
+ // Alert callouts and task lists are both a marker at the head of a
329
+ // paragraph, so both are handled before inline parsing turns that paragraph
330
+ // into children: rewriting `content` here is all it takes.
331
+ md.core.ruler.before('inline', 'github_extras', (state) => {
332
+ const tokens = state.tokens;
333
+ for (let i = 0; i < tokens.length; i++) {
334
+ const token = tokens[i];
335
+ if (token.type === 'blockquote_open' &&
336
+ tokens[i + 1]?.type === 'paragraph_open' &&
337
+ tokens[i + 2]?.type === 'inline') {
338
+ const m = /^\[!(note|tip|important|warning|caution)\][ \t]*(\r?\n|$)/i.exec(tokens[i + 2].content);
339
+ if (m) {
340
+ const kind = m[1].toLowerCase();
341
+ token.attrJoin('class', `alert alert-${kind}`);
342
+ const rest = tokens[i + 2].content.slice(m[0].length);
343
+ tokens[i + 2].content = rest;
344
+ if (rest.trim() === '') {
345
+ tokens[i + 1].hidden = true;
346
+ tokens[i + 3].hidden = true;
347
+ }
348
+ const title = new state.Token('html_block', '', 0);
349
+ title.content = `<p class="alert-title">${ALERTS[kind]}</p>\n`;
350
+ tokens.splice(i + 1, 0, title);
351
+ i += 1;
352
+ }
353
+ continue;
354
+ }
355
+ if (token.type === 'inline' &&
356
+ tokens[i - 1]?.type === 'paragraph_open' &&
357
+ tokens[i - 2]?.type === 'list_item_open') {
358
+ const m = /^\[([ xX])\][ \t]+/.exec(token.content);
359
+ if (m) {
360
+ const checked = m[1].toLowerCase() === 'x' ? ' checked' : '';
361
+ token.content = `<input type="checkbox" disabled${checked}> ${token.content.slice(m[0].length)}`;
362
+ tokens[i - 2].attrJoin('class', 'task-item');
363
+ }
364
+ }
365
+ }
366
+ });
367
+ return md;
368
+ }
369
+ // Built once, at load. Every render's state rides in the env instead.
370
+ const MD = buildMarkdownIt();
371
+ function renderMarkdown(text, opts) {
372
+ const env = {
373
+ opts,
374
+ slots: [],
375
+ // Random per render, so a document cannot forge a slot marker.
376
+ marker: `md${crypto.randomBytes(9).toString('hex')}`,
377
+ used: new Map(),
378
+ pendingSlug: '',
379
+ };
380
+ const html = (0, sanitize_html_1.default)(MD.render(text, env), sanitizeOptions(opts));
381
+ return html.replace(new RegExp(`${env.marker}_(\\d+)_`, 'g'), (_all, n) => env.slots[Number(n)] ?? '');
382
+ }