@raspberrypifoundation/rpf-markdown-core 0.1.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/README.md +37 -0
- package/dist/index.cjs +1217 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1181 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { marked as marked2 } from "marked";
|
|
3
|
+
import { gfmHeadingId } from "marked-gfm-heading-id";
|
|
4
|
+
|
|
5
|
+
// src/block_renderers.ts
|
|
6
|
+
import { marked } from "marked";
|
|
7
|
+
|
|
8
|
+
// src/shared_utils.ts
|
|
9
|
+
function escapeHtml(value) {
|
|
10
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/block_renderers.ts
|
|
14
|
+
var CALLOUT_HEADINGS = {
|
|
15
|
+
debug: "Debugging",
|
|
16
|
+
tip: "Tip"
|
|
17
|
+
};
|
|
18
|
+
function renderMarkdown(input) {
|
|
19
|
+
return marked.parse(input);
|
|
20
|
+
}
|
|
21
|
+
function renderAccordion({
|
|
22
|
+
modifier,
|
|
23
|
+
heading,
|
|
24
|
+
body,
|
|
25
|
+
contentClass = "c-project-panel__content u-hidden",
|
|
26
|
+
bodyIsHtml = false
|
|
27
|
+
}) {
|
|
28
|
+
const escapedHeading = escapeHtml(heading);
|
|
29
|
+
const renderedBody = bodyIsHtml ? normalizePreservedHtml(body) : renderMarkdown(body);
|
|
30
|
+
const normalizedBody = normalizePanelHtml(renderedBody);
|
|
31
|
+
return [
|
|
32
|
+
`<div class="c-project-panel c-project-panel--${modifier}">`,
|
|
33
|
+
' <h3 class="c-project-panel__heading js-project-panel__toggle">',
|
|
34
|
+
` ${escapedHeading}`,
|
|
35
|
+
" </h3>",
|
|
36
|
+
"",
|
|
37
|
+
` <div class="${contentClass}">`,
|
|
38
|
+
` ${normalizedBody.trimEnd()}`,
|
|
39
|
+
"",
|
|
40
|
+
" </div>",
|
|
41
|
+
"</div>"
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
function renderTaskBlock(body) {
|
|
45
|
+
const renderedBody = renderMarkdown(body);
|
|
46
|
+
return [
|
|
47
|
+
'<div class="c-project-task">',
|
|
48
|
+
' <input class="c-project-task__checkbox" type="checkbox" aria-label="Mark this task as complete" />',
|
|
49
|
+
' <div class="c-project-task__body">',
|
|
50
|
+
` ${renderedBody.trimEnd()}`,
|
|
51
|
+
"",
|
|
52
|
+
" </div>",
|
|
53
|
+
"</div>"
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
function renderHintsPanel(bodies) {
|
|
57
|
+
const slides = bodies.map(
|
|
58
|
+
(body) => [
|
|
59
|
+
'<div class="c-project-panel__swiper-slide">',
|
|
60
|
+
` ${renderMarkdown(body).trimEnd()}`,
|
|
61
|
+
"",
|
|
62
|
+
"</div>"
|
|
63
|
+
].join("\n")
|
|
64
|
+
).join("\n\n");
|
|
65
|
+
return [
|
|
66
|
+
'<div class="c-project-panel c-project-panel--hints">',
|
|
67
|
+
' <h3 class="c-project-panel__heading js-project-panel__toggle">',
|
|
68
|
+
" I need a hint",
|
|
69
|
+
" </h3>",
|
|
70
|
+
"",
|
|
71
|
+
' <div class="c-project-panel__content js-project-panel--initialise-swiper u-hidden">',
|
|
72
|
+
' <div class="c-project-panel__swiper">',
|
|
73
|
+
' <div class="c-project-panel__swiper-wrapper">',
|
|
74
|
+
` ${slides}`,
|
|
75
|
+
" </div>",
|
|
76
|
+
"",
|
|
77
|
+
' <div class="c-project-panel__swiper-pagination">',
|
|
78
|
+
' <span class="c-project-panel__swiper-bullet"></span>',
|
|
79
|
+
' <span class="c-project-panel__swiper-bullet"></span>',
|
|
80
|
+
' <span class="c-project-panel__swiper-bullet"></span>',
|
|
81
|
+
" </div>",
|
|
82
|
+
"",
|
|
83
|
+
' <div class="c-project-panel__swiper-button c-project-panel__swiper-button--next"></div>',
|
|
84
|
+
' <div class="c-project-panel__swiper-button c-project-panel__swiper-button--prev"></div>',
|
|
85
|
+
" </div>",
|
|
86
|
+
" </div>",
|
|
87
|
+
"</div>"
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
function renderHintSlide(body) {
|
|
91
|
+
return [
|
|
92
|
+
'<div class="c-project-panel__swiper-slide">',
|
|
93
|
+
` ${renderMarkdown(body).trimEnd()}`,
|
|
94
|
+
"",
|
|
95
|
+
"</div>"
|
|
96
|
+
].join("\n");
|
|
97
|
+
}
|
|
98
|
+
function renderCalloutBlock(type, body) {
|
|
99
|
+
const heading = CALLOUT_HEADINGS[type] ?? type;
|
|
100
|
+
return renderExistingCalloutBlock(type, `### ${heading}
|
|
101
|
+
|
|
102
|
+
${body}`);
|
|
103
|
+
}
|
|
104
|
+
function renderExistingCalloutBlock(type, body) {
|
|
105
|
+
return [
|
|
106
|
+
`<div class="c-project-callout c-project-callout--${type}">`,
|
|
107
|
+
"",
|
|
108
|
+
renderMarkdown(body).trimEnd(),
|
|
109
|
+
"",
|
|
110
|
+
"</div>"
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
function renderSaveBlock() {
|
|
114
|
+
return [
|
|
115
|
+
'<div class="c-project-panel c-project-panel--save">',
|
|
116
|
+
' <h3 class="c-project-panel__heading">',
|
|
117
|
+
" Save your project",
|
|
118
|
+
" </h3>",
|
|
119
|
+
"</div>"
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
function renderNoPrintBlock(body) {
|
|
123
|
+
return `<div class="u-no-print">
|
|
124
|
+
|
|
125
|
+
${renderMarkdown(body).trimEnd()}
|
|
126
|
+
</div>`;
|
|
127
|
+
}
|
|
128
|
+
function renderPrintOnlyBlock(body) {
|
|
129
|
+
return `<div class="u-print-only">
|
|
130
|
+
|
|
131
|
+
${renderMarkdown(body).trimEnd()}
|
|
132
|
+
</div>`;
|
|
133
|
+
}
|
|
134
|
+
function renderChallengeBlock(body, title = "") {
|
|
135
|
+
const content = title ? `## ${title}
|
|
136
|
+
|
|
137
|
+
${body}` : body;
|
|
138
|
+
return renderMarkdown(content).trimEnd();
|
|
139
|
+
}
|
|
140
|
+
function renderInfoBlock(body) {
|
|
141
|
+
return [
|
|
142
|
+
'<div style="border-left: solid; border-width:10px; border-color: #0faeb0; background-color: aliceblue; padding: 10px;">',
|
|
143
|
+
renderMarkdown(body).trimEnd(),
|
|
144
|
+
"</div>"
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
147
|
+
function normalizePreservedHtml(body) {
|
|
148
|
+
return body.trim();
|
|
149
|
+
}
|
|
150
|
+
function normalizePanelHtml(body) {
|
|
151
|
+
return body.replace(
|
|
152
|
+
/<div class="c-code-filename">\s*([^<\n][\s\S]*?)\s*<\/div>/g,
|
|
153
|
+
(_match, filename) => `<div class="c-code-filename">
|
|
154
|
+
<p>${filename.trim()}</p>
|
|
155
|
+
</div>`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/regex_constants.ts
|
|
160
|
+
var SCRATCHBLOCKS_FENCED_BLOCK_REGEX = /^[ \t]*(`{3,}|~{3,})(blocks[23]?)[^\r\n]*\r?\n([\s\S]*?)\r?\n?[ \t]*\1[ \t]*$/;
|
|
161
|
+
var RPF_CODE_OPEN_REGEX = /^---\s+code\s+---$/;
|
|
162
|
+
var RPF_CODE_CLOSE_REGEX = /^---\s+\/code\s+---$/;
|
|
163
|
+
var RPF_CODE_FRONTMATTER_WRAPPED_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/;
|
|
164
|
+
var RPF_CODE_FRONTMATTER_BARE_REGEX = /^((?:[^\n]+\n)*?)(\n[\s\S]*)$/;
|
|
165
|
+
var RPF_CODE_KNOWN_KEYS_REGEX = /^(language|line_numbers|line_number_start|line_numbers_start|line_highlights)\s*:/;
|
|
166
|
+
var CALLOUT_OPEN_REGEX = /^<div\s+class="(c-project-callout\s+c-project-callout--([a-z0-9-]+))">/;
|
|
167
|
+
var TASK_OPEN_REGEX = /^---\s+task\s+---$/;
|
|
168
|
+
var TASK_CLOSE_REGEX = /^---\s+\/task\s+---$/;
|
|
169
|
+
var OUTPUT_OPEN_REGEX = /^<div\s+class="c-project-output">\s*$/;
|
|
170
|
+
var MARKER_LINE_REGEX = /^\[!(ACCORDION|CHALLENGE|DEBUG|HINT|NOPRINT|PRINTONLY|SAVE|TASK|TIP|INFO)\](?:[ \t]+([^\n]*))?[ \t]*$/;
|
|
171
|
+
|
|
172
|
+
// src/scratchblocks.ts
|
|
173
|
+
import initScratchblocks from "scratchblocks/index.js";
|
|
174
|
+
var SCRATCHBLOCKS_PARSE_ERROR = "Input does not appear to be a valid scratchblocks fenced code block. Expected opening fence with info string 'blocks', 'blocks2', or 'blocks3'.";
|
|
175
|
+
var SCRATCHBLOCKS_NO_RUNTIME_ERROR = "Scratchblocks rendering requires a browser window or a Node runtime where happy-dom can be required.";
|
|
176
|
+
var rendererCache;
|
|
177
|
+
var warnedAboutHappyDomLoadFailure = false;
|
|
178
|
+
function mapFenceInfoToStyle(info) {
|
|
179
|
+
const tag = info.trim().toLowerCase();
|
|
180
|
+
if (tag === "blocks2") return "scratch2";
|
|
181
|
+
return "scratch3";
|
|
182
|
+
}
|
|
183
|
+
function isCompatibleScratchblocksWindow(value) {
|
|
184
|
+
if (value == null || typeof value !== "object") {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const maybeWindow = value;
|
|
188
|
+
return maybeWindow.document != null && maybeWindow.DOMParser != null && maybeWindow.HTMLCanvasElement != null;
|
|
189
|
+
}
|
|
190
|
+
function getGlobalBrowserWindow() {
|
|
191
|
+
const maybeWindow = globalThis.window;
|
|
192
|
+
return isCompatibleScratchblocksWindow(maybeWindow) ? maybeWindow : void 0;
|
|
193
|
+
}
|
|
194
|
+
function resolveNodeRequire() {
|
|
195
|
+
const nodeProcess = globalThis.process;
|
|
196
|
+
const moduleBuiltin = nodeProcess?.getBuiltinModule?.("module");
|
|
197
|
+
const createRequire = moduleBuiltin?.createRequire;
|
|
198
|
+
if (typeof createRequire === "function") {
|
|
199
|
+
return createRequire(import.meta.url);
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const maybeRequire = (0, eval)("require");
|
|
203
|
+
if (typeof maybeRequire === "function") {
|
|
204
|
+
return maybeRequire;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
function createNodeRuntimeWindow() {
|
|
212
|
+
const requireFn = resolveNodeRequire();
|
|
213
|
+
if (requireFn == null) {
|
|
214
|
+
return void 0;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const happyDomModule = requireFn("happy-dom");
|
|
218
|
+
const HappyDomWindow = happyDomModule.Window;
|
|
219
|
+
if (typeof HappyDomWindow !== "function") {
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
return new HappyDomWindow();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (!warnedAboutHappyDomLoadFailure) {
|
|
225
|
+
warnedAboutHappyDomLoadFailure = true;
|
|
226
|
+
console.warn(
|
|
227
|
+
"Unable to load happy-dom for server-side scratchblocks rendering:",
|
|
228
|
+
error
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function ensureXmlHasCreateCDATASection(xml) {
|
|
235
|
+
if (typeof xml.createCDATASection !== "function") {
|
|
236
|
+
xml.createCDATASection = (data) => xml.createTextNode(data);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function doesWindowNeedCDataSectionPolyfill(window) {
|
|
240
|
+
try {
|
|
241
|
+
const parserCtor = window.DOMParser;
|
|
242
|
+
const parser = new parserCtor();
|
|
243
|
+
const xml = parser.parseFromString("<xml/>", "application/xml");
|
|
244
|
+
return typeof xml.createCDATASection !== "function";
|
|
245
|
+
} catch {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function applyCDataSectionPolyfill(window) {
|
|
250
|
+
if (!doesWindowNeedCDataSectionPolyfill(window)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const parserPrototype = window.DOMParser.prototype;
|
|
254
|
+
if (typeof parserPrototype.parseFromString === "function") {
|
|
255
|
+
try {
|
|
256
|
+
const originalParse = parserPrototype.parseFromString.bind(parserPrototype);
|
|
257
|
+
parserPrototype.parseFromString = (markup, mimeType) => {
|
|
258
|
+
const xml = originalParse(markup, mimeType);
|
|
259
|
+
ensureXmlHasCreateCDATASection(xml);
|
|
260
|
+
return xml;
|
|
261
|
+
};
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const createDocument = window.document.implementation?.createDocument;
|
|
266
|
+
if (typeof createDocument === "function") {
|
|
267
|
+
try {
|
|
268
|
+
const implementation = window.document.implementation;
|
|
269
|
+
if (implementation == null) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const originalCreateDocument = createDocument.bind(implementation);
|
|
273
|
+
implementation.createDocument = (namespace, qualifiedNameStr, documentType) => {
|
|
274
|
+
const xml = originalCreateDocument(
|
|
275
|
+
namespace,
|
|
276
|
+
qualifiedNameStr,
|
|
277
|
+
documentType
|
|
278
|
+
);
|
|
279
|
+
ensureXmlHasCreateCDATASection(xml);
|
|
280
|
+
return xml;
|
|
281
|
+
};
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function doesWindowNeedCanvasContextPolyfill(window) {
|
|
287
|
+
try {
|
|
288
|
+
const maybeCanvas = window.document.createElement?.("canvas");
|
|
289
|
+
return maybeCanvas == null || typeof maybeCanvas.getContext !== "function" || maybeCanvas.getContext("2d") == null;
|
|
290
|
+
} catch {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function applyCanvasContextPolyfill(window) {
|
|
295
|
+
if (!doesWindowNeedCanvasContextPolyfill(window)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const canvasPrototype = window.HTMLCanvasElement.prototype;
|
|
299
|
+
if (canvasPrototype == null || typeof canvasPrototype.getContext !== "function") {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const originalGetContext = canvasPrototype.getContext;
|
|
303
|
+
try {
|
|
304
|
+
canvasPrototype.getContext = function getContext(contextId) {
|
|
305
|
+
if (contextId !== "2d") {
|
|
306
|
+
return originalGetContext.call(this, contextId);
|
|
307
|
+
}
|
|
308
|
+
const context2d = originalGetContext.call(this, contextId);
|
|
309
|
+
if (context2d != null) {
|
|
310
|
+
return context2d;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
font: "",
|
|
314
|
+
measureText(value) {
|
|
315
|
+
return { width: value.length * 7 };
|
|
316
|
+
},
|
|
317
|
+
save() {
|
|
318
|
+
},
|
|
319
|
+
scale() {
|
|
320
|
+
},
|
|
321
|
+
drawImage() {
|
|
322
|
+
},
|
|
323
|
+
restore() {
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function extractInjectedScratchblocksStyles(headBefore, headAfter) {
|
|
331
|
+
if (headAfter.startsWith(headBefore)) {
|
|
332
|
+
const injected = headAfter.slice(headBefore.length).trim();
|
|
333
|
+
if (injected !== "") {
|
|
334
|
+
return injected;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const styleTags = headAfter.match(/<style[^>]*>[\s\S]*?<\/style>/g) ?? [];
|
|
338
|
+
const scratchblocksStyleTags = styleTags.filter(
|
|
339
|
+
(tag) => tag.includes(".sb2-") || tag.includes(".sb3-")
|
|
340
|
+
);
|
|
341
|
+
return scratchblocksStyleTags.join("");
|
|
342
|
+
}
|
|
343
|
+
function embedStylesIntoSvg(svg, stylesHtml) {
|
|
344
|
+
const styles = stylesHtml.trim();
|
|
345
|
+
if (styles === "") {
|
|
346
|
+
return svg;
|
|
347
|
+
}
|
|
348
|
+
const openTagEnd = svg.indexOf(">");
|
|
349
|
+
if (openTagEnd === -1) {
|
|
350
|
+
return `${styles}${svg}`;
|
|
351
|
+
}
|
|
352
|
+
return `${svg.slice(0, openTagEnd + 1)}${styles}${svg.slice(openTagEnd + 1)}`;
|
|
353
|
+
}
|
|
354
|
+
function createScratchblocksRenderer() {
|
|
355
|
+
const browserWindow = getGlobalBrowserWindow();
|
|
356
|
+
const renderWindow = browserWindow ?? createNodeRuntimeWindow();
|
|
357
|
+
if (renderWindow == null) {
|
|
358
|
+
throw new Error(SCRATCHBLOCKS_NO_RUNTIME_ERROR);
|
|
359
|
+
}
|
|
360
|
+
applyCDataSectionPolyfill(renderWindow);
|
|
361
|
+
applyCanvasContextPolyfill(renderWindow);
|
|
362
|
+
const headBefore = renderWindow.document.head.innerHTML;
|
|
363
|
+
const api = initScratchblocks(renderWindow);
|
|
364
|
+
api.appendStyles();
|
|
365
|
+
const headAfter = renderWindow.document.head.innerHTML;
|
|
366
|
+
const stylesHtml = extractInjectedScratchblocksStyles(headBefore, headAfter);
|
|
367
|
+
return { api, stylesHtml };
|
|
368
|
+
}
|
|
369
|
+
function getOrCreateScratchblocksRenderer() {
|
|
370
|
+
if (rendererCache === void 0) {
|
|
371
|
+
rendererCache = createScratchblocksRenderer();
|
|
372
|
+
}
|
|
373
|
+
return rendererCache;
|
|
374
|
+
}
|
|
375
|
+
function parseScratchblocksBlock(fencedBlock) {
|
|
376
|
+
const fenceMatch = fencedBlock.match(SCRATCHBLOCKS_FENCED_BLOCK_REGEX);
|
|
377
|
+
if (!fenceMatch) {
|
|
378
|
+
throw new Error(SCRATCHBLOCKS_PARSE_ERROR);
|
|
379
|
+
}
|
|
380
|
+
const infoString = fenceMatch[2] ?? "blocks3";
|
|
381
|
+
const code = fenceMatch[3] ?? "";
|
|
382
|
+
const style = mapFenceInfoToStyle(infoString);
|
|
383
|
+
try {
|
|
384
|
+
const renderer = getOrCreateScratchblocksRenderer();
|
|
385
|
+
const doc = renderer.api.parse(code, { languages: ["en"] });
|
|
386
|
+
const svgElement = renderer.api.render(doc, {
|
|
387
|
+
style,
|
|
388
|
+
languages: ["en"],
|
|
389
|
+
scale: 1
|
|
390
|
+
});
|
|
391
|
+
const svg = embedStylesIntoSvg(svgElement.outerHTML, renderer.stylesHtml);
|
|
392
|
+
const html = `<div class="c-project-scratchblocks" data-style="${style}">${svg}</div>`;
|
|
393
|
+
return { html, style, svg };
|
|
394
|
+
} catch (error) {
|
|
395
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
396
|
+
throw new Error(`Failed to render scratchblocks SVG: ${message}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/codeblocks.ts
|
|
401
|
+
function renderStandardCodeBlock(text, language) {
|
|
402
|
+
const codeClass = language ? ` class="language-${language}"` : "";
|
|
403
|
+
return `<pre><code${codeClass}>${escapeCode(text)}
|
|
404
|
+
</code></pre>`;
|
|
405
|
+
}
|
|
406
|
+
function hasProjectCodeOptions(info) {
|
|
407
|
+
return /\b(?:filename|line_numbers|line_numbers_start|line_number_start|line_highlights)=/.test(
|
|
408
|
+
info
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
function extractFenceBody(rawFence, fallback) {
|
|
412
|
+
if (rawFence === void 0) return fallback;
|
|
413
|
+
return parseFencedBlock(rawFence)?.code ?? fallback;
|
|
414
|
+
}
|
|
415
|
+
function extractListItemFenceBlocks(rawListItem) {
|
|
416
|
+
const lines = rawListItem.replace(/\r\n/g, "\n").split("\n");
|
|
417
|
+
const blocks = [];
|
|
418
|
+
for (let index = 0; index < lines.length; index++) {
|
|
419
|
+
const openingLine = lines[index] ?? "";
|
|
420
|
+
const openingMatch = openingLine.match(/^[ \t]*(`{3,}|~{3,})([^\n]*)$/);
|
|
421
|
+
if (!openingMatch) continue;
|
|
422
|
+
const fence = openingMatch[1] ?? "";
|
|
423
|
+
const blockLines = [openingLine];
|
|
424
|
+
const closingRegex = new RegExp(`^[ \\t]*${fence}[ \\t]*$`);
|
|
425
|
+
for (index++; index < lines.length; index++) {
|
|
426
|
+
const line = lines[index] ?? "";
|
|
427
|
+
blockLines.push(line);
|
|
428
|
+
if (closingRegex.test(line)) {
|
|
429
|
+
blocks.push(normalizeListItemFence(blockLines.join("\n")));
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return blocks;
|
|
435
|
+
}
|
|
436
|
+
function renderMarkedCodeToken(token, rawFenceOverride) {
|
|
437
|
+
const info = (token.info ?? token.lang ?? "").trim();
|
|
438
|
+
const tag = (info.split(/\s+/)[0] ?? "").toLowerCase();
|
|
439
|
+
const rawFence = rawFenceOverride ?? (typeof token.raw === "string" && /^[ \t]*(?:`{3,}|~{3,})/.test(token.raw) ? token.raw : void 0);
|
|
440
|
+
const codeText = extractFenceBody(rawFence, token.text);
|
|
441
|
+
const fenceBlock = rawFence ?? `\`\`\`${info}
|
|
442
|
+
${token.text}
|
|
443
|
+
\`\`\``;
|
|
444
|
+
if (["blocks", "blocks2", "blocks3"].includes(tag)) {
|
|
445
|
+
try {
|
|
446
|
+
return `${parseScratchblocksBlock(fenceBlock).html}
|
|
447
|
+
`;
|
|
448
|
+
} catch {
|
|
449
|
+
return `${renderStandardCodeBlock(codeText, "")}
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (hasProjectCodeOptions(info)) {
|
|
454
|
+
try {
|
|
455
|
+
return `${highlightCodeBlock(fenceBlock).html}
|
|
456
|
+
`;
|
|
457
|
+
} catch {
|
|
458
|
+
return `${renderStandardCodeBlock(codeText, tag)}
|
|
459
|
+
`;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return `${renderStandardCodeBlock(codeText, tag)}
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
465
|
+
function applyInlineCodeClasses(html) {
|
|
466
|
+
return html.replace(
|
|
467
|
+
/<code>([\s\S]*?)<\/code>\{:\s*class=["”]([^"”]+)["”]\}/g,
|
|
468
|
+
(_match, codeContent, className) => `<code class="${escapeHtml(className)}">${codeContent}</code>`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
function escapeCode(value) {
|
|
472
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
473
|
+
}
|
|
474
|
+
function createInvalidBlockError(blockType, expected) {
|
|
475
|
+
if (expected === void 0) {
|
|
476
|
+
return new Error(`Input does not appear to be a valid ${blockType}.`);
|
|
477
|
+
}
|
|
478
|
+
return new Error(
|
|
479
|
+
`Input does not appear to be a valid ${blockType}. Expected ${expected}.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
function applyCodeBlockOption(key, value, options, allowSingularLineNumberStart) {
|
|
483
|
+
const normalizedValue = stripWrappingQuotes(value);
|
|
484
|
+
switch (key) {
|
|
485
|
+
case "filename":
|
|
486
|
+
options.filename = normalizedValue;
|
|
487
|
+
break;
|
|
488
|
+
case "line_numbers":
|
|
489
|
+
options.line_numbers = normalizedValue === "true";
|
|
490
|
+
break;
|
|
491
|
+
case "line_numbers_start":
|
|
492
|
+
options.line_numbers_start = parseInt(normalizedValue, 10);
|
|
493
|
+
break;
|
|
494
|
+
case "line_number_start":
|
|
495
|
+
if (allowSingularLineNumberStart) {
|
|
496
|
+
options.line_numbers_start = parseInt(normalizedValue, 10);
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
case "line_highlights":
|
|
500
|
+
options.line_highlights = normalizeLineHighlights(normalizedValue);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function stripWrappingQuotes(value) {
|
|
505
|
+
const trimmed = value.trim();
|
|
506
|
+
const first = trimmed[0];
|
|
507
|
+
const last = trimmed.at(-1);
|
|
508
|
+
if (first === '"' && last === '"' || first === "'" && last === "'") {
|
|
509
|
+
return trimmed.slice(1, -1);
|
|
510
|
+
}
|
|
511
|
+
return trimmed;
|
|
512
|
+
}
|
|
513
|
+
function normalizeLineHighlights(value) {
|
|
514
|
+
return value.split(",").map((part) => part.trim()).filter((part) => part.length > 0).join(", ");
|
|
515
|
+
}
|
|
516
|
+
function parseFenceInfo(infoString) {
|
|
517
|
+
const parts = infoString.trim().match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
518
|
+
const language = parts[0] ?? "";
|
|
519
|
+
const options = {};
|
|
520
|
+
for (let i = 1; i < parts.length; i++) {
|
|
521
|
+
const part = parts[i];
|
|
522
|
+
if (part === void 0) continue;
|
|
523
|
+
const eqIdx = part.indexOf("=");
|
|
524
|
+
if (eqIdx === -1) continue;
|
|
525
|
+
const key = part.slice(0, eqIdx);
|
|
526
|
+
const value = part.slice(eqIdx + 1);
|
|
527
|
+
applyCodeBlockOption(key, value, options, true);
|
|
528
|
+
}
|
|
529
|
+
return { language, options };
|
|
530
|
+
}
|
|
531
|
+
function highlightCodeBlock(fencedBlock) {
|
|
532
|
+
const fence = parseFencedBlock(fencedBlock);
|
|
533
|
+
if (fence === void 0) {
|
|
534
|
+
throw createInvalidBlockError("fenced code block");
|
|
535
|
+
}
|
|
536
|
+
const infoString = fence.infoString.trim();
|
|
537
|
+
const code = fence.code;
|
|
538
|
+
const { language, options } = parseFenceInfo(infoString);
|
|
539
|
+
const html = buildCodeHtml(code, language, options);
|
|
540
|
+
return { html, language, options };
|
|
541
|
+
}
|
|
542
|
+
function parseFencedBlock(fencedBlock) {
|
|
543
|
+
const normalized = fencedBlock.replace(/\r\n/g, "\n").trimEnd();
|
|
544
|
+
const lines = normalized.split("\n");
|
|
545
|
+
const openingLine = lines[0] ?? "";
|
|
546
|
+
const closingLine = lines.at(-1) ?? "";
|
|
547
|
+
const openingMatch = openingLine.match(/^[ \t]*(`{3,}|~{3,})([^\n]*)$/);
|
|
548
|
+
if (!openingMatch) return void 0;
|
|
549
|
+
const fence = openingMatch[1] ?? "";
|
|
550
|
+
const closingRegex = new RegExp(`^[ \\t]*${fence}[ \\t]*$`);
|
|
551
|
+
if (!closingRegex.test(closingLine)) return void 0;
|
|
552
|
+
return {
|
|
553
|
+
infoString: openingMatch[2] ?? "",
|
|
554
|
+
code: lines.slice(1, -1).join("\n")
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function normalizeListItemFence(rawFence) {
|
|
558
|
+
const normalized = rawFence.replace(/\r\n/g, "\n").trimEnd();
|
|
559
|
+
const lines = normalized.split("\n");
|
|
560
|
+
const openingIndent = lines[0]?.match(/^[ \t]*/)?.[0] ?? "";
|
|
561
|
+
const openingLine = lines[0]?.trimStart() ?? "";
|
|
562
|
+
const closingLine = lines.at(-1)?.trimStart() ?? "";
|
|
563
|
+
const codeLines = lines.slice(1, -1).map((line) => {
|
|
564
|
+
const stripped = line.startsWith(openingIndent) ? line.slice(openingIndent.length) : line;
|
|
565
|
+
return stripped.length > 0 ? ` ${stripped}` : stripped;
|
|
566
|
+
});
|
|
567
|
+
return [openingLine, ...codeLines, closingLine].join("\n");
|
|
568
|
+
}
|
|
569
|
+
function buildCodeHtml(code, language, options) {
|
|
570
|
+
const highlightedCode = escapeProjectCode(code);
|
|
571
|
+
const preClasses = [];
|
|
572
|
+
if (options.line_numbers === true) preClasses.push("line-numbers");
|
|
573
|
+
if (options.line_numbers === false) preClasses.push("no-line-numbers");
|
|
574
|
+
const preClass = preClasses.length > 0 ? ` class="${preClasses.join(" ")}"` : "";
|
|
575
|
+
const dataStart = options.line_numbers_start !== void 0 ? ` data-start="${options.line_numbers_start}"` : "";
|
|
576
|
+
const dataLine = options.line_highlights !== void 0 ? ` data-line="${options.line_highlights}"` : "";
|
|
577
|
+
const dataLineOffset = options.line_numbers_start !== void 0 && options.line_highlights !== void 0 ? ` data-line-offset="${options.line_numbers_start}"` : "";
|
|
578
|
+
const codeClass = language ? ` class="language-${language}"` : "";
|
|
579
|
+
const codeOutput = `<pre dir="ltr"${preClass}${dataStart}${dataLineOffset}${dataLine}><code${codeClass} dir="ltr">
|
|
580
|
+
${highlightedCode}
|
|
581
|
+
</code></pre>`;
|
|
582
|
+
return options.filename ? `<div class="c-code-filename">
|
|
583
|
+
${escapeHtml(options.filename)}
|
|
584
|
+
</div>
|
|
585
|
+
${codeOutput}` : codeOutput;
|
|
586
|
+
}
|
|
587
|
+
function escapeProjectCode(code) {
|
|
588
|
+
return code.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
589
|
+
}
|
|
590
|
+
function parseRpfCodeFrontmatter(frontmatter) {
|
|
591
|
+
let language = "";
|
|
592
|
+
const options = {};
|
|
593
|
+
for (const line of frontmatter.split("\n")) {
|
|
594
|
+
const colonIdx = line.indexOf(":");
|
|
595
|
+
if (colonIdx === -1) continue;
|
|
596
|
+
const key = line.slice(0, colonIdx).trim();
|
|
597
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
598
|
+
if (key === "language") {
|
|
599
|
+
if (language === "") language = value;
|
|
600
|
+
} else {
|
|
601
|
+
applyCodeBlockOption(key, value, options, true);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return { language, options };
|
|
605
|
+
}
|
|
606
|
+
function parseRpfCodeBlock(input) {
|
|
607
|
+
const trimmed = input.trim();
|
|
608
|
+
const lines = trimmed.split("\n");
|
|
609
|
+
const firstLine = lines[0]?.trim() ?? "";
|
|
610
|
+
if (!RPF_CODE_OPEN_REGEX.test(firstLine)) {
|
|
611
|
+
throw createInvalidBlockError("RPF code block", "opening --- code ---");
|
|
612
|
+
}
|
|
613
|
+
const lastLine = lines.at(-1)?.trim() ?? "";
|
|
614
|
+
if (!RPF_CODE_CLOSE_REGEX.test(lastLine)) {
|
|
615
|
+
throw createInvalidBlockError("RPF code block", "closing --- /code ---");
|
|
616
|
+
}
|
|
617
|
+
const inner = lines.slice(1, -1).join("\n").trimStart();
|
|
618
|
+
let language = "";
|
|
619
|
+
let options = {};
|
|
620
|
+
let codeBody = inner;
|
|
621
|
+
const fmWrappedMatch = inner.match(RPF_CODE_FRONTMATTER_WRAPPED_REGEX);
|
|
622
|
+
const fmBareMatch = !fmWrappedMatch && inner.match(RPF_CODE_FRONTMATTER_BARE_REGEX);
|
|
623
|
+
if (fmWrappedMatch) {
|
|
624
|
+
({ language, options } = parseRpfCodeFrontmatter(fmWrappedMatch[1] ?? ""));
|
|
625
|
+
codeBody = fmWrappedMatch[2] ?? "";
|
|
626
|
+
} else if (fmBareMatch && fmBareMatch[1] && fmBareMatch[1].split("\n").every((l) => l === "" || RPF_CODE_KNOWN_KEYS_REGEX.test(l))) {
|
|
627
|
+
({ language, options } = parseRpfCodeFrontmatter(fmBareMatch[1]));
|
|
628
|
+
codeBody = fmBareMatch[2] ?? "";
|
|
629
|
+
}
|
|
630
|
+
const code = codeBody.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
631
|
+
const html = buildCodeHtml(code, language, options);
|
|
632
|
+
return { html, language, options };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/marked-smartypants-lite.ts
|
|
636
|
+
function smartenInlineText(value) {
|
|
637
|
+
return value.replace(/---/g, "\u2014").replace(/--/g, "\u2013").replace(/(^|[-\u2014/(\[{"\s])'/g, "$1\u2018").replace(/'/g, "\u2019").replace(/(^|[-\u2014/(\[{\u2018\s])"/g, "$1\u201C").replace(/"/g, "\u201D").replace(/\.{3}/g, "\u2026");
|
|
638
|
+
}
|
|
639
|
+
function markedSmartypantsLite() {
|
|
640
|
+
return {
|
|
641
|
+
tokenizer: {
|
|
642
|
+
inlineText(src) {
|
|
643
|
+
const cap = this.rules.inline.text.exec(src);
|
|
644
|
+
if (!cap) {
|
|
645
|
+
return void 0;
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
type: "text",
|
|
649
|
+
raw: cap[0],
|
|
650
|
+
text: smartenInlineText(cap[0])
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/rfm.ts
|
|
658
|
+
function parseMarkerLine(line) {
|
|
659
|
+
const match = line.trim().match(MARKER_LINE_REGEX);
|
|
660
|
+
if (!match) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
"Input does not appear to be a valid RFM block. Expected opening marker line such as [!TASK]."
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
type: match[1],
|
|
667
|
+
title: (match[2] ?? "").trim()
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function removeOneQuoteLevel(input) {
|
|
671
|
+
return input.split("\n").map((line) => line.replace(/^[ \t]*>[ \t]?/, "")).join("\n").trim();
|
|
672
|
+
}
|
|
673
|
+
function parseBlock(input) {
|
|
674
|
+
const inner = removeOneQuoteLevel(input);
|
|
675
|
+
const lines = inner.split("\n");
|
|
676
|
+
const marker = parseMarkerLine(lines[0] ?? "");
|
|
677
|
+
const body = lines.slice(1).join("\n").replace(/^\s*\n/, "").trim();
|
|
678
|
+
return { ...marker, body };
|
|
679
|
+
}
|
|
680
|
+
function matchRfmBlock(src) {
|
|
681
|
+
const firstLine = src.split("\n", 1)[0] ?? "";
|
|
682
|
+
const openingMarker = getRfmMarker(firstLine);
|
|
683
|
+
if (openingMarker === void 0) return void 0;
|
|
684
|
+
const lines = src.split("\n");
|
|
685
|
+
const collected = [firstLine];
|
|
686
|
+
for (let index = 1; index < lines.length; index++) {
|
|
687
|
+
const line = lines[index] ?? "";
|
|
688
|
+
const marker = getRfmMarker(line);
|
|
689
|
+
if (marker !== void 0 && marker.depth <= openingMarker.depth && !shouldGroupRfmMarkers(openingMarker, marker)) {
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
if (line.trim() === "") {
|
|
693
|
+
const nextContentLine = lines.slice(index + 1).find((candidate) => candidate.trim() !== "");
|
|
694
|
+
const nextMarker = nextContentLine === void 0 ? void 0 : getRfmMarker(nextContentLine);
|
|
695
|
+
if (nextMarker !== void 0 && nextMarker.depth <= openingMarker.depth && !shouldGroupRfmMarkers(openingMarker, nextMarker)) {
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
collected.push(line);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (/^[ \t]*>/.test(line)) {
|
|
702
|
+
collected.push(line);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
while (collected.at(-1)?.trim() === "") collected.pop();
|
|
708
|
+
return collected.length > 0 ? collected.join("\n") : void 0;
|
|
709
|
+
}
|
|
710
|
+
function getRfmMarker(line) {
|
|
711
|
+
const match = line.match(/^[ \t]*(>+)[ \t]?\[!(\w+)\]/);
|
|
712
|
+
const type = match?.[2];
|
|
713
|
+
if (match?.[1] === void 0 || type === void 0) return void 0;
|
|
714
|
+
return { depth: match[1].length, type };
|
|
715
|
+
}
|
|
716
|
+
function shouldGroupRfmMarkers(openingMarker, nextMarker) {
|
|
717
|
+
return openingMarker.depth === nextMarker.depth && openingMarker.type === "HINT" && nextMarker.type === "HINT";
|
|
718
|
+
}
|
|
719
|
+
function splitTopLevelRfmHintBlocks(input) {
|
|
720
|
+
const blocks = [];
|
|
721
|
+
const lines = input.split("\n");
|
|
722
|
+
let current = [];
|
|
723
|
+
for (const line of lines) {
|
|
724
|
+
if (current.length > 0 && line.trim() !== "" && /^[ \t]*>+[ \t]?\[!/.test(line)) {
|
|
725
|
+
blocks.push(current.join("\n"));
|
|
726
|
+
current = [line];
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
current.push(line);
|
|
730
|
+
}
|
|
731
|
+
if (current.length > 0) blocks.push(current.join("\n"));
|
|
732
|
+
return blocks.filter(isRfmHintBlock);
|
|
733
|
+
}
|
|
734
|
+
function parseRfmBlock(input) {
|
|
735
|
+
const block = parseBlock(input);
|
|
736
|
+
let html;
|
|
737
|
+
switch (block.type) {
|
|
738
|
+
case "ACCORDION":
|
|
739
|
+
html = renderAccordion({
|
|
740
|
+
modifier: "ingredient",
|
|
741
|
+
heading: block.title,
|
|
742
|
+
body: block.body
|
|
743
|
+
});
|
|
744
|
+
break;
|
|
745
|
+
case "TASK":
|
|
746
|
+
html = renderTaskBlock(block.body);
|
|
747
|
+
break;
|
|
748
|
+
case "HINT":
|
|
749
|
+
html = renderHintsPanel([block.body]);
|
|
750
|
+
break;
|
|
751
|
+
case "TIP":
|
|
752
|
+
case "DEBUG":
|
|
753
|
+
html = renderCalloutBlock(block.type.toLowerCase(), block.body);
|
|
754
|
+
break;
|
|
755
|
+
case "SAVE":
|
|
756
|
+
html = renderSaveBlock();
|
|
757
|
+
break;
|
|
758
|
+
case "NOPRINT":
|
|
759
|
+
html = renderNoPrintBlock(block.body);
|
|
760
|
+
break;
|
|
761
|
+
case "PRINTONLY":
|
|
762
|
+
html = renderPrintOnlyBlock(block.body);
|
|
763
|
+
break;
|
|
764
|
+
case "CHALLENGE":
|
|
765
|
+
html = renderChallengeBlock(block.body, block.title);
|
|
766
|
+
break;
|
|
767
|
+
case "INFO":
|
|
768
|
+
html = renderInfoBlock(block.body);
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
return { html, type: block.type, title: block.title };
|
|
772
|
+
}
|
|
773
|
+
function getRfmHintBody(input) {
|
|
774
|
+
const block = parseBlock(input);
|
|
775
|
+
if (block.type !== "HINT") {
|
|
776
|
+
throw new Error("Expected an RFM HINT block.");
|
|
777
|
+
}
|
|
778
|
+
return block.body;
|
|
779
|
+
}
|
|
780
|
+
function isRfmHintBlock(input) {
|
|
781
|
+
try {
|
|
782
|
+
return parseBlock(input).type === "HINT";
|
|
783
|
+
} catch {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/rpfblocks.ts
|
|
789
|
+
function createInvalidBlockError2(blockType, expected) {
|
|
790
|
+
return new Error(
|
|
791
|
+
`Input does not appear to be a valid ${blockType}. Expected ${expected}.`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
function matchLegacyBlock(src, blockName) {
|
|
795
|
+
return new RegExp(
|
|
796
|
+
`^[ \\t]*---\\s+${blockName}\\s+---[ \\t]*\\n[\\s\\S]*?\\n[ \\t]*---\\s+\\/${blockName}\\s+---[ \\t]*(?:\\n|$)`
|
|
797
|
+
).exec(src);
|
|
798
|
+
}
|
|
799
|
+
function parseCalloutBlock(input) {
|
|
800
|
+
const trimmed = input.trim();
|
|
801
|
+
const lines = trimmed.split("\n");
|
|
802
|
+
const firstLine = lines[0] ?? "";
|
|
803
|
+
const openMatch = firstLine.match(CALLOUT_OPEN_REGEX);
|
|
804
|
+
if (!openMatch) {
|
|
805
|
+
throw createInvalidBlockError2(
|
|
806
|
+
"RPF callout block",
|
|
807
|
+
'opening <div class="c-project-callout c-project-callout--{type}">'
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
const type = openMatch[2] ?? "";
|
|
811
|
+
const lastLine = lines.at(-1)?.trim() ?? "";
|
|
812
|
+
if (lastLine !== "</div>") {
|
|
813
|
+
throw createInvalidBlockError2("RPF callout block", "closing </div>");
|
|
814
|
+
}
|
|
815
|
+
const inner = trimmed.slice(firstLine.length).slice(0, -"</div>".length).trim();
|
|
816
|
+
const html = renderExistingCalloutBlock(type, inner);
|
|
817
|
+
return { html, type };
|
|
818
|
+
}
|
|
819
|
+
function parseTaskBlock(input) {
|
|
820
|
+
const trimmed = input.trim();
|
|
821
|
+
const lines = trimmed.split("\n");
|
|
822
|
+
const firstLine = lines[0]?.trim() ?? "";
|
|
823
|
+
if (!TASK_OPEN_REGEX.test(firstLine)) {
|
|
824
|
+
throw createInvalidBlockError2("RPF task block", "opening --- task ---");
|
|
825
|
+
}
|
|
826
|
+
const lastLine = lines.at(-1)?.trim() ?? "";
|
|
827
|
+
if (!TASK_CLOSE_REGEX.test(lastLine)) {
|
|
828
|
+
throw createInvalidBlockError2("RPF task block", "closing --- /task ---");
|
|
829
|
+
}
|
|
830
|
+
const inner = lines.slice(1, -1).join("\n").trim();
|
|
831
|
+
const html = renderTaskBlock(inner);
|
|
832
|
+
return { html };
|
|
833
|
+
}
|
|
834
|
+
function stripOuterMarkerBlock(input, blockName) {
|
|
835
|
+
const dedented = dedentBlock(input.trim());
|
|
836
|
+
const lines = dedented.split("\n");
|
|
837
|
+
const openRegex = new RegExp(`^---\\s+${blockName}\\s+---$`);
|
|
838
|
+
const closeRegex = new RegExp(`^---\\s+\\/${blockName}\\s+---$`);
|
|
839
|
+
const firstLineIndex = lines.findIndex((line) => openRegex.test(line.trim()));
|
|
840
|
+
const lastLineIndex = lines.findLastIndex(
|
|
841
|
+
(line) => closeRegex.test(line.trim())
|
|
842
|
+
);
|
|
843
|
+
if (firstLineIndex === -1 || lastLineIndex === -1 || lastLineIndex <= firstLineIndex) {
|
|
844
|
+
throw createInvalidBlockError2(
|
|
845
|
+
`RPF ${blockName} block`,
|
|
846
|
+
`opening --- ${blockName} --- and closing --- /${blockName} ---`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return lines.slice(firstLineIndex + 1, lastLineIndex).join("\n").trim();
|
|
850
|
+
}
|
|
851
|
+
function dedentBlock(input) {
|
|
852
|
+
const indents = input.split("\n").filter((line) => line.trim().length > 0).map((line) => line.match(/^[ \t]*/)?.[0].length ?? 0);
|
|
853
|
+
const minIndent = Math.min(...indents);
|
|
854
|
+
if (!Number.isFinite(minIndent) || minIndent === 0) return input;
|
|
855
|
+
return input.split("\n").map((line) => line.slice(minIndent)).join("\n");
|
|
856
|
+
}
|
|
857
|
+
function parseLegacyFrontmatter(input) {
|
|
858
|
+
const match = input.trimStart().match(/^[ \t]*---[ \t]*\n([\s\S]*?)\n[ \t]*---[ \t]*\n?([\s\S]*)$/);
|
|
859
|
+
if (!match) {
|
|
860
|
+
return { attributes: {}, body: input.trim() };
|
|
861
|
+
}
|
|
862
|
+
const attributes = {};
|
|
863
|
+
for (const line of (match[1] ?? "").split("\n")) {
|
|
864
|
+
const colonIdx = line.indexOf(":");
|
|
865
|
+
if (colonIdx === -1) continue;
|
|
866
|
+
const key = line.slice(0, colonIdx).trim();
|
|
867
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
868
|
+
if (key.length > 0) attributes[key] = value;
|
|
869
|
+
}
|
|
870
|
+
return { attributes, body: (match[2] ?? "").trim() };
|
|
871
|
+
}
|
|
872
|
+
function parseCollapseBlock(input) {
|
|
873
|
+
const inner = stripOuterMarkerBlock(input, "collapse");
|
|
874
|
+
const { attributes, body } = parseLegacyFrontmatter(inner);
|
|
875
|
+
return renderAccordion({
|
|
876
|
+
modifier: "ingredient",
|
|
877
|
+
heading: attributes.title ?? "",
|
|
878
|
+
body,
|
|
879
|
+
bodyIsHtml: /^\s*</.test(body)
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
function parseChallengeBlock(input) {
|
|
883
|
+
return renderChallengeBlock(stripOuterMarkerBlock(input, "challenge"));
|
|
884
|
+
}
|
|
885
|
+
function parseNoPrintBlock(input) {
|
|
886
|
+
return renderNoPrintBlock(stripOuterMarkerBlock(input, "no-print"));
|
|
887
|
+
}
|
|
888
|
+
function parsePrintOnlyBlock(input) {
|
|
889
|
+
return renderPrintOnlyBlock(stripOuterMarkerBlock(input, "print-only"));
|
|
890
|
+
}
|
|
891
|
+
function parseSaveBlock() {
|
|
892
|
+
return renderSaveBlock();
|
|
893
|
+
}
|
|
894
|
+
function parseHintBlock(input) {
|
|
895
|
+
return renderHintSlide(stripOuterMarkerBlock(input, "hint"));
|
|
896
|
+
}
|
|
897
|
+
function parseHintsBlock(input) {
|
|
898
|
+
const inner = stripOuterMarkerBlock(input, "hints");
|
|
899
|
+
const bodies = [
|
|
900
|
+
...inner.matchAll(/^---\s+hint\s+---$([\s\S]*?)^---\s+\/hint\s+---$/gm)
|
|
901
|
+
].map((match) => (match[1] ?? "").trim());
|
|
902
|
+
return renderHintsPanel(bodies);
|
|
903
|
+
}
|
|
904
|
+
function parseOutputBlock(input) {
|
|
905
|
+
const trimmed = input.trim();
|
|
906
|
+
const lines = trimmed.split("\n");
|
|
907
|
+
const firstLine = lines[0]?.trim() ?? "";
|
|
908
|
+
if (!OUTPUT_OPEN_REGEX.test(firstLine)) {
|
|
909
|
+
createInvalidBlockError2(
|
|
910
|
+
"RPF output block",
|
|
911
|
+
'opening <div class="c-project-output">'
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
const lastLine = lines.at(-1)?.trim() ?? "";
|
|
915
|
+
if (lastLine !== "</div>") {
|
|
916
|
+
createInvalidBlockError2("RPF output block", "closing </div>");
|
|
917
|
+
}
|
|
918
|
+
const inner = trimmed.slice(firstLine.length).slice(0, -"</div>".length).trim();
|
|
919
|
+
return `<div class="c-project-output">${inner}</div>`;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/index.ts
|
|
923
|
+
function createLegacyBlockExtension(name, blockName, render, appendNewline = true) {
|
|
924
|
+
const startRegex = new RegExp(`^[ \\t]*--- ${blockName} ---$`, "m");
|
|
925
|
+
return {
|
|
926
|
+
name,
|
|
927
|
+
level: "block",
|
|
928
|
+
start(src) {
|
|
929
|
+
return src.search(startRegex);
|
|
930
|
+
},
|
|
931
|
+
tokenizer(src) {
|
|
932
|
+
const match = matchLegacyBlock(src, blockName);
|
|
933
|
+
if (match) {
|
|
934
|
+
return { type: name, raw: match[0], text: match[0] };
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
renderer(token) {
|
|
938
|
+
const html = render(token.text);
|
|
939
|
+
return appendNewline ? `${html}
|
|
940
|
+
` : html;
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function processEditorProject(content) {
|
|
945
|
+
marked2.use(markedSmartypantsLite());
|
|
946
|
+
marked2.use({
|
|
947
|
+
extensions: [
|
|
948
|
+
{
|
|
949
|
+
name: "htmlLineBreak",
|
|
950
|
+
level: "block",
|
|
951
|
+
start(src) {
|
|
952
|
+
return src.search(/^[ \t]*\{\.page-break\}[ \t]*(?:\n|$)/m);
|
|
953
|
+
},
|
|
954
|
+
tokenizer(src) {
|
|
955
|
+
const match = /^[ \t]*\{\.page-break\}[ \t]*(?:\n|$)/.exec(src);
|
|
956
|
+
if (match) {
|
|
957
|
+
return { type: "htmlLineBreak", raw: match[0] };
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
renderer() {
|
|
961
|
+
return "<div class=page-break></div>\n";
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
name: "preservedFence",
|
|
966
|
+
level: "block",
|
|
967
|
+
start(src) {
|
|
968
|
+
return src.search(/^[ \t]*(`{3,}|~{3,})/m);
|
|
969
|
+
},
|
|
970
|
+
tokenizer(src) {
|
|
971
|
+
const match = src.match(
|
|
972
|
+
/^[ \t]*(`{3,}|~{3,})([^\r\n]*)\r?\n([\s\S]*?)\r?\n[ \t]*\1[ \t]*(?:\n|$)/
|
|
973
|
+
);
|
|
974
|
+
if (!match) return;
|
|
975
|
+
return {
|
|
976
|
+
type: "preservedFence",
|
|
977
|
+
raw: match[0],
|
|
978
|
+
text: match[3] ?? "",
|
|
979
|
+
info: (match[2] ?? "").trim()
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
renderer(token) {
|
|
983
|
+
return renderMarkedCodeToken(token, token.raw);
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
// RFM alert blocks: > [!TASK], > [!ACCORDION], etc.
|
|
987
|
+
{
|
|
988
|
+
name: "rfmBlock",
|
|
989
|
+
level: "block",
|
|
990
|
+
start(src) {
|
|
991
|
+
return src.search(/^[ \t]*>+[ \t]?\[!/m);
|
|
992
|
+
},
|
|
993
|
+
tokenizer(src) {
|
|
994
|
+
const raw = matchRfmBlock(src);
|
|
995
|
+
if (raw !== void 0) {
|
|
996
|
+
return { type: "rfmBlock", raw, text: raw };
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
renderer(token) {
|
|
1000
|
+
const raw = token.text;
|
|
1001
|
+
const hintBlocks = splitTopLevelRfmHintBlocks(raw);
|
|
1002
|
+
if (hintBlocks.length > 1) {
|
|
1003
|
+
return `${renderHintsPanel(hintBlocks.map(getRfmHintBody))}
|
|
1004
|
+
`;
|
|
1005
|
+
}
|
|
1006
|
+
return `${parseRfmBlock(raw).html}
|
|
1007
|
+
`;
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
// RPF callout blocks: <div class="c-project-callout c-project-callout--{type}">
|
|
1011
|
+
{
|
|
1012
|
+
name: "rpfCallout",
|
|
1013
|
+
level: "block",
|
|
1014
|
+
start(src) {
|
|
1015
|
+
return src.search(
|
|
1016
|
+
/<div\s+class="c-project-callout\s+c-project-callout--/
|
|
1017
|
+
);
|
|
1018
|
+
},
|
|
1019
|
+
tokenizer(src) {
|
|
1020
|
+
const match = /^<div\s+class="c-project-callout\s+c-project-callout--[a-z0-9-]+">[\s\S]*?<\/div>/.exec(
|
|
1021
|
+
src
|
|
1022
|
+
);
|
|
1023
|
+
if (match) {
|
|
1024
|
+
return { type: "rpfCallout", raw: match[0], text: match[0] };
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
renderer(token) {
|
|
1028
|
+
return `${parseCalloutBlock(token.text).html}
|
|
1029
|
+
`;
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
// RPF task blocks: --- task --- ... --- /task ---
|
|
1033
|
+
{
|
|
1034
|
+
name: "rpfTask",
|
|
1035
|
+
level: "block",
|
|
1036
|
+
start(src) {
|
|
1037
|
+
return src.search(/^--- task ---$/m);
|
|
1038
|
+
},
|
|
1039
|
+
tokenizer(src) {
|
|
1040
|
+
const match = /^--- task ---\n[\s\S]*?\n--- \/task ---/.exec(src);
|
|
1041
|
+
if (match) {
|
|
1042
|
+
return { type: "rpfTask", raw: match[0], text: match[0] };
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
renderer(token) {
|
|
1046
|
+
return `${parseTaskBlock(token.text).html}
|
|
1047
|
+
`;
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
// RPF collapse blocks: --- collapse --- ... --- /collapse ---
|
|
1051
|
+
createLegacyBlockExtension("rpfCollapse", "collapse", parseCollapseBlock),
|
|
1052
|
+
// RPF hints blocks: --- hints --- ... --- /hints ---
|
|
1053
|
+
createLegacyBlockExtension("rpfHints", "hints", parseHintsBlock),
|
|
1054
|
+
// RPF hint blocks: --- hint --- ... --- /hint ---
|
|
1055
|
+
createLegacyBlockExtension("rpfHint", "hint", parseHintBlock),
|
|
1056
|
+
// RPF challenge blocks: --- challenge --- ... --- /challenge ---
|
|
1057
|
+
createLegacyBlockExtension(
|
|
1058
|
+
"rpfChallenge",
|
|
1059
|
+
"challenge",
|
|
1060
|
+
parseChallengeBlock,
|
|
1061
|
+
false
|
|
1062
|
+
),
|
|
1063
|
+
// RPF print visibility blocks.
|
|
1064
|
+
createLegacyBlockExtension("rpfNoPrint", "no-print", parseNoPrintBlock),
|
|
1065
|
+
createLegacyBlockExtension(
|
|
1066
|
+
"rpfPrintOnly",
|
|
1067
|
+
"print-only",
|
|
1068
|
+
parsePrintOnlyBlock
|
|
1069
|
+
),
|
|
1070
|
+
// RPF save block: --- save ---
|
|
1071
|
+
{
|
|
1072
|
+
name: "rpfSave",
|
|
1073
|
+
level: "block",
|
|
1074
|
+
start(src) {
|
|
1075
|
+
return src.search(/^[ \t]*--- save ---$/m);
|
|
1076
|
+
},
|
|
1077
|
+
tokenizer(src) {
|
|
1078
|
+
const match = /^[ \t]*---\s+save\s+---[ \t]*(?:\n|$)/.exec(src);
|
|
1079
|
+
if (match) {
|
|
1080
|
+
return { type: "rpfSave", raw: match[0], text: match[0] };
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
renderer() {
|
|
1084
|
+
return `${parseSaveBlock()}
|
|
1085
|
+
`;
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1088
|
+
// RPF code blocks: --- code --- ... --- /code ---
|
|
1089
|
+
// Also handles the wrapped form: <div class="c-project-code">\n--- code ---\n...\n--- /code ---\n</div>
|
|
1090
|
+
{
|
|
1091
|
+
name: "rpfCode",
|
|
1092
|
+
level: "block",
|
|
1093
|
+
start(src) {
|
|
1094
|
+
const bare = src.search(/^--- code ---$/m);
|
|
1095
|
+
const wrapped = src.search(/^<div class="c-project-code">/m);
|
|
1096
|
+
if (bare === -1) return wrapped;
|
|
1097
|
+
if (wrapped === -1) return bare;
|
|
1098
|
+
return Math.min(bare, wrapped);
|
|
1099
|
+
},
|
|
1100
|
+
tokenizer(src) {
|
|
1101
|
+
const wrappedMatch = /^<div class="c-project-code">\n(--- code ---\n[\s\S]*?\n--- \/code ---)\n\s*<\/div>/.exec(
|
|
1102
|
+
src
|
|
1103
|
+
);
|
|
1104
|
+
if (wrappedMatch) {
|
|
1105
|
+
return {
|
|
1106
|
+
type: "rpfCode",
|
|
1107
|
+
raw: wrappedMatch[0],
|
|
1108
|
+
text: wrappedMatch[1]
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
const match = /^--- code ---\n[\s\S]*?\n--- \/code ---/.exec(src);
|
|
1112
|
+
if (match) {
|
|
1113
|
+
return { type: "rpfCode", raw: match[0], text: match[0] };
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
renderer(token) {
|
|
1117
|
+
return `${parseRpfCodeBlock(token.text).html}
|
|
1118
|
+
`;
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
// RPF output blocks: <div class="c-project-output"> ... </div>
|
|
1122
|
+
{
|
|
1123
|
+
name: "rpfOutput",
|
|
1124
|
+
level: "block",
|
|
1125
|
+
start(src) {
|
|
1126
|
+
return src.search(/^<div\s+class="c-project-output">\s*$/m);
|
|
1127
|
+
},
|
|
1128
|
+
tokenizer(src) {
|
|
1129
|
+
const match = /^<div\s+class="c-project-output">\s[\s\S]*?<\/div>/.exec(src);
|
|
1130
|
+
if (match) {
|
|
1131
|
+
return { type: "rpfOutput", raw: match[0], text: match[0] };
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
renderer(token) {
|
|
1135
|
+
return `${parseOutputBlock(token.text)}
|
|
1136
|
+
`;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
],
|
|
1140
|
+
renderer: {
|
|
1141
|
+
// Override fenced code blocks to use Prism with our custom options.
|
|
1142
|
+
code(token) {
|
|
1143
|
+
return renderMarkedCodeToken(token);
|
|
1144
|
+
},
|
|
1145
|
+
listitem(item) {
|
|
1146
|
+
const itemTokens = Array.isArray(item.tokens) ? item.tokens : [];
|
|
1147
|
+
const raw = typeof item.raw === "string" ? item.raw : "";
|
|
1148
|
+
const rawFenceBlocks = extractListItemFenceBlocks(raw);
|
|
1149
|
+
let rawFenceIndex = 0;
|
|
1150
|
+
const firstNewline = raw.indexOf("\n");
|
|
1151
|
+
const hasBlankLineAfterMarker = firstNewline !== -1 && /^\n[ \t]*\n/.test(raw.slice(firstNewline));
|
|
1152
|
+
let body = "";
|
|
1153
|
+
for (let index = 0; index < itemTokens.length; index++) {
|
|
1154
|
+
const token = itemTokens[index];
|
|
1155
|
+
if (index === 0 && !item.task && !hasBlankLineAfterMarker && itemTokens.length > 1 && token?.type === "paragraph") {
|
|
1156
|
+
body += this.parser.parseInline(token.tokens ?? []);
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
if (token?.type === "code" || token?.type === "preservedFence") {
|
|
1160
|
+
body += renderMarkedCodeToken(token, rawFenceBlocks[rawFenceIndex]);
|
|
1161
|
+
rawFenceIndex++;
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
body += this.parser.parse([token]).trimStart();
|
|
1165
|
+
}
|
|
1166
|
+
return `<li>${body}</li>
|
|
1167
|
+
`;
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
hooks: {
|
|
1171
|
+
postprocess(html) {
|
|
1172
|
+
return applyInlineCodeClasses(html);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
marked2.use(gfmHeadingId({ prefix: "" }));
|
|
1177
|
+
return marked2.parse(content);
|
|
1178
|
+
}
|
|
1179
|
+
export {
|
|
1180
|
+
processEditorProject
|
|
1181
|
+
};
|