mr-md 2.0.0-beta → 2.1.1-beta

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.
@@ -1,5 +1,5 @@
1
1
  import { escAttr, escHtml, renderBlock } from "./blocks.js";
2
- import { clientScript, pageCSS, renderPage } from "./html.js"; // Used in renderChapter
2
+ import { renderPage, renderLayout } from "./html.js"; // Used in renderChapter
3
3
  // ─── Main render function ─────────────────────────────────────────────────────
4
4
  export function render(lesson, opts = {}) {
5
5
  const bodyItems = [];
@@ -296,89 +296,7 @@ export function renderChapter(chapter, opts = {}) {
296
296
  }
297
297
  }
298
298
  `;
299
- return `<!DOCTYPE html>
300
- <html lang="en" data-palette="${palette}" data-ui="${ui}" ${schemeAttr}>
301
- <head>
302
- <meta charset="UTF-8">
303
- <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650;9..144,760&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&family=Archivo:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Syne:wght@600;700;800&family=Playfair+Display:ital,wght@0,400..700;1,400..700&family=Lora:ital,wght@0,400..700;1,400..700&display=swap" rel="stylesheet">
304
- <meta name="viewport" content="width=device-width, initial-scale=1">
305
- <title>${escHtml(chapter.meta.title)}</title>
306
- ${chapter.meta.description ? `<meta name="description" content="${escHtml(chapter.meta.description)}">` : ""}
307
- ${opts.head ?? ""}
308
- <style>
309
- ${opts.font ? `:root { --font-sans: ${opts.font}, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }` : ""}
310
- ${pageCSS()}
311
- ${chapterStyles}
312
- </style>
313
- </head>
314
- <body class="bk-layout-${layout} bk-density-${density} bk-tone-${tone}">
315
- <div class="bk-shell">
316
- <aside class="bk-sidebar">
317
- <div class="bk-sidebar-inner">
318
- <div class="bk-sidebar-header">
319
- <div style="margin-top: 8px;"></div>
320
- <div class="bk-sidebar-title">${escHtml(chapter.meta.title)}</div>
321
- </div>
322
- <nav class="bk-nav">${navHtml}</nav>
323
- <div class="bk-sidebar-footer">
324
- <button class="bk-icon-btn bk-settings-button" id="bk-settings-button" type="button" aria-expanded="false" aria-controls="bk-theme-panel" title="Display settings">
325
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
326
- <span class="bk-sr-only">Display settings</span>
327
- </button>
328
- <div class="bk-theme-panel" id="bk-theme-panel" aria-label="Display settings" hidden>
329
- <div class="bk-theme-row">
330
- <span>Theme</span>
331
- <div class="bk-segmented-control" id="bk-theme-icons">
332
- <button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
333
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
334
- </button>
335
- <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : (!theme ? "active" : "")}" data-theme="auto" title="System" aria-label="System theme">
336
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
337
- </button>
338
- <button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
339
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
340
- </button>
341
- </div>
342
- </div>
343
- <div class="bk-theme-row">
344
- <span>Palette</span>
345
- <div class="bk-segmented-control" id="bk-palette-icons">
346
- <button type="button" class="bk-segment-btn ${palette === "ink" ? "active" : ""}" data-palette="ink" title="Ink" aria-label="Ink palette">
347
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path></svg>
348
- </button>
349
- <button type="button" class="bk-segment-btn ${palette === "field" ? "active" : ""}" data-palette="field" title="Field" aria-label="Field palette">
350
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"></path><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 12"></path></svg>
351
- </button>
352
- <button type="button" class="bk-segment-btn ${palette === "ember" ? "active" : ""}" data-palette="ember" title="Ember" aria-label="Ember palette">
353
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0011 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 11-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 002.5 2.5z"></path></svg>
354
- </button>
355
- </div>
356
- </div>
357
- <div class="bk-theme-row">
358
- <span>UI</span>
359
- <div class="bk-segmented-control" id="bk-ui-icons">
360
- <button type="button" class="bk-segment-btn ${ui === 'standard' ? 'active' : ''}" data-ui="standard" title="Standard" aria-label="Standard UI">
361
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="4" ry="4"></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg>
362
- </button>
363
- <button type="button" class="bk-segment-btn ${ui === 'neo' ? 'active' : ''}" data-ui="neo" title="Neo Brutalist" aria-label="Neo UI">
364
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><rect x="3" y="3" width="18" height="18"></rect><path d="M3 10h18"></path><path d="M10 10v11"></path></svg>
365
- </button>
366
- <button type="button" class="bk-segment-btn ${ui === 'playful' ? 'active' : ''}" data-ui="playful" title="Playful" aria-label="Playful UI">
367
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="6" ry="6"></rect><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor"></circle><circle cx="15.5" cy="15.5" r="1.5" fill="currentColor"></circle></svg>
368
- </button>
369
- </div>
370
- </div>
371
- </div>
372
- </div>
373
- </div>
374
- </aside>
375
- <button class="bk-sidebar-collapse-floating" id="bk-sidebar-collapse" aria-label="Collapse sidebar">
376
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
377
- </button>
378
- <main class="bk-main">
379
- <button class="bk-sidebar-expand" id="bk-sidebar-expand" type="button" aria-label="Expand sidebar">
380
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
381
- </button>
299
+ const contentHtml = `
382
300
  <article class="bk-content" style="max-width: 1000px; margin: 0 auto;">
383
301
  <header class="bk-hero" style="border-bottom: none;">
384
302
  <p class="bk-eyebrow">Chapter</p>
@@ -387,11 +305,7 @@ ${chapterStyles}
387
305
  </header>
388
306
  ${timelineHtml}
389
307
  </article>
390
- </main>
391
- </div>
392
- <script>
393
- ${clientScript()}
394
- </script>
395
- </body>
396
- </html>`;
308
+ `;
309
+ const headHtml = `<style>${chapterStyles}</style>`;
310
+ return renderLayout(chapter.meta.title, chapter.meta.description, navHtml, contentHtml, { ...opts, head: (opts.head ?? "") + headHtml });
397
311
  }
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/renderer/markdown.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAYzC,iBAAS,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAmFtH;AAeD,iBAAS,WAAW,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,SAAY,EAClB,aAAa,UAAO,EACpB,EAAE,CAAC,EAAE,MAAM,GACT,MAAM,CAYR;AAED,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBtC;AAED,iBAAS,wBAAwB,CAChC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,GAC3C,MAAM,CAiCR;AAED,iBAAS,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIhD;AAED,OAAO,EACN,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,wBAAwB,GACxB,CAAC"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/renderer/markdown.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAYzC,iBAAS,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAoFtH;AAeD,iBAAS,WAAW,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,SAAY,EAClB,aAAa,UAAO,EACpB,EAAE,CAAC,EAAE,MAAM,GACT,MAAM,CAYR;AAED,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBtC;AAED,iBAAS,wBAAwB,CAChC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,GAC3C,MAAM,CAwER;AAED,iBAAS,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIhD;AAED,OAAO,EACN,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,wBAAwB,GACxB,CAAC"}
@@ -39,13 +39,14 @@ function mdToHtml(md) {
39
39
  });
40
40
  // Restore code blocks
41
41
  codeBlocks.forEach((match, id) => {
42
- processedMd = processedMd.replace(`@@BK_CODE_${id}@@`, () => match);
42
+ processedMd = processedMd.replaceAll(`@@BK_CODE_${id}@@`, () => match);
43
43
  });
44
44
  const headings = [];
45
+ const idPrefix = Math.random().toString(36).substring(2, 6);
45
46
  let headingIdCounter = 0;
46
47
  const renderer = new marked.Renderer();
47
48
  renderer.heading = ({ tokens, depth, text }) => {
48
- const id = `bk-heading-${headingIdCounter++}`;
49
+ const id = `bk-heading-${idPrefix}-${headingIdCounter++}`;
49
50
  if (depth === 1 || depth === 2) {
50
51
  const plainText = text.replace(/<[^>]+>/g, "");
51
52
  headings.push({ id, text: plainText, level: depth });
@@ -60,16 +61,16 @@ function mdToHtml(md) {
60
61
  displayMode: true,
61
62
  });
62
63
  // marked might wrap block placeholders in <p>
63
- html = html.replace(`<p>@@BK_MATH_BLOCK_${id}@@</p>`, () => `<div class="bk-math-block">${rendered}</div>`);
64
+ html = html.replaceAll(`<p>@@BK_MATH_BLOCK_${id}@@</p>`, () => `<div class="bk-math-block">${rendered}</div>`);
64
65
  // Fallback if not wrapped in <p>
65
- html = html.replace(`@@BK_MATH_BLOCK_${id}@@`, () => `<div class="bk-math-block">${rendered}</div>`);
66
+ html = html.replaceAll(`@@BK_MATH_BLOCK_${id}@@`, () => `<div class="bk-math-block">${rendered}</div>`);
66
67
  });
67
68
  mathInlines.forEach((tex, id) => {
68
69
  const rendered = katex.renderToString(tex, {
69
70
  throwOnError: false,
70
71
  displayMode: false,
71
72
  });
72
- html = html.replace(`@@BK_MATH_INLINE_${id}@@`, () => rendered);
73
+ html = html.replaceAll(`@@BK_MATH_INLINE_${id}@@`, () => rendered);
73
74
  });
74
75
  return { html, title, headings };
75
76
  }
@@ -110,7 +111,7 @@ function mdInline(text) {
110
111
  throwOnError: false,
111
112
  displayMode: false,
112
113
  });
113
- html = html.replace(`@@BK_MATH_INLINE_${id}@@`, () => rendered);
114
+ html = html.replaceAll(`@@BK_MATH_INLINE_${id}@@`, () => rendered);
114
115
  });
115
116
  return html;
116
117
  }
@@ -118,22 +119,58 @@ function renderSimulationControls(block) {
118
119
  const props = block.props ?? {};
119
120
  const keys = Object.keys(block.tunables ?? props).filter((key) => {
120
121
  const value = props[key];
121
- return typeof value === "number" || typeof value === "boolean";
122
+ return typeof value === "number" || typeof value === "boolean" || typeof value === "string";
122
123
  });
123
124
  if (!keys.length || block.controls === "observe")
124
125
  return "";
125
- return `<div class="bk-sim-controls" aria-label="Simulation controls">
126
- ${keys
127
- .map((key) => {
126
+ const controls = keys.map((key) => {
128
127
  const value = props[key];
129
128
  const control = block.tunables?.[key] ?? {};
129
+ let type = control.type;
130
+ if (!type) {
131
+ if (typeof value === "boolean")
132
+ type = "boolean";
133
+ else if (typeof value === "string")
134
+ type = "text";
135
+ else
136
+ type = "range";
137
+ }
130
138
  const label = escHtml(control.label ?? key.replace(/([A-Z])/g, " $1"));
131
- if (typeof value === "boolean") {
132
- return `<label class="bk-sim-toggle">
139
+ return { key, value, control, type, label };
140
+ });
141
+ const ranges = controls.filter((c) => c.type === "range");
142
+ const booleans = controls.filter((c) => c.type === "boolean");
143
+ const others = controls.filter((c) => c.type === "text" || c.type === "number");
144
+ const sortedControls = [...ranges, ...others, ...booleans];
145
+ let firstBooleanRendered = false;
146
+ return `<div class="bk-sim-controls" aria-label="Simulation controls">
147
+ ${sortedControls
148
+ .map(({ key, value, control, type, label }) => {
149
+ if (type === "boolean") {
150
+ const isFirst = !firstBooleanRendered;
151
+ firstBooleanRendered = true;
152
+ const extraClass = isFirst ? " bk-sim-toggle--first" : "";
153
+ return `<label class="bk-sim-toggle${extraClass}">
133
154
  <input type="checkbox" data-bk-prop="${escAttr(key)}" ${value ? "checked" : ""}>
134
155
  <span>${label}</span>
135
156
  </label>`;
136
157
  }
158
+ if (type === "text") {
159
+ return `<label class="bk-sim-text">
160
+ <span>${label}</span>
161
+ <input type="text" data-bk-prop="${escAttr(key)}" value="${escAttr(String(value))}">
162
+ </label>`;
163
+ }
164
+ if (type === "number") {
165
+ const min = control.min ?? "";
166
+ const max = control.max ?? "";
167
+ const step = control.step ?? "any";
168
+ return `<label class="bk-sim-number">
169
+ <span>${label}</span>
170
+ <input type="number" data-bk-prop="${escAttr(key)}" min="${min}" max="${max}" step="${step}" value="${value}">
171
+ </label>`;
172
+ }
173
+ // type === "range"
137
174
  const min = control.min ?? Math.min(0, Number(value));
138
175
  const max = control.max ?? Math.max(10, Number(value) * 2);
139
176
  const step = control.step ?? 1;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/renderer/utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,CAAC;CACpD;AAID,iBAAS,cAAc,CACtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,YAAY,GAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,MAAe,GAClD,MAAM,CAqDR;AAED,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CA0CnE;AAED,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/renderer/utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,CAAC;CACpD;AAID,iBAAS,cAAc,CACtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,YAAY,GAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,MAAe,GAClD,MAAM,CA0DR;AAED,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CAuDnE;AAED,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC"}
@@ -27,6 +27,10 @@ function resolveContent(src, options, expectedType = "text") {
27
27
  filePath = fallbackPath;
28
28
  }
29
29
  }
30
+ const baseDir = path.resolve(options.contentBase ?? ".");
31
+ if (!filePath.startsWith(baseDir) && options.strict !== false) {
32
+ throw new Error(`Security Error: Path traversal attempt outside contentBase: ${filePath}`);
33
+ }
30
34
  if (fs.existsSync(filePath)) {
31
35
  const stat = fs.statSync(filePath);
32
36
  if (stat.isFile()) {
@@ -52,12 +56,23 @@ function resolveContent(src, options, expectedType = "text") {
52
56
  function resolveAssetSrc(src, options) {
53
57
  if (/^(https?:|data:)/.test(src))
54
58
  return src;
55
- let isWebAbsolute = src.startsWith("/") && !fs.existsSync(src);
56
- let filePath = path.isAbsolute(src)
57
- ? src
58
- : path.resolve(options.contentBase ?? ".", src);
59
- if (src.startsWith("/") && !fs.existsSync(filePath)) {
60
- const fallbackPath = path.resolve(options.contentBase ?? ".", src.slice(1));
59
+ const hashIndex = src.indexOf("#");
60
+ const queryIndex = src.indexOf("?");
61
+ const breakIndex = hashIndex !== -1 && queryIndex !== -1
62
+ ? Math.min(hashIndex, queryIndex)
63
+ : Math.max(hashIndex, queryIndex);
64
+ let cleanSrc = src;
65
+ let suffix = "";
66
+ if (breakIndex !== -1) {
67
+ cleanSrc = src.substring(0, breakIndex);
68
+ suffix = src.substring(breakIndex);
69
+ }
70
+ let isWebAbsolute = cleanSrc.startsWith("/") && !fs.existsSync(cleanSrc);
71
+ let filePath = path.isAbsolute(cleanSrc)
72
+ ? cleanSrc
73
+ : path.resolve(options.contentBase ?? ".", cleanSrc);
74
+ if (cleanSrc.startsWith("/") && !fs.existsSync(filePath)) {
75
+ const fallbackPath = path.resolve(options.contentBase ?? ".", cleanSrc.slice(1));
61
76
  if (fs.existsSync(fallbackPath)) {
62
77
  filePath = fallbackPath;
63
78
  isWebAbsolute = false; // We found it locally, so don't treat it as a web URL
@@ -84,6 +99,6 @@ function resolveAssetSrc(src, options) {
84
99
  if (!fs.existsSync(outPath)) {
85
100
  fs.copyFileSync(filePath, outPath);
86
101
  }
87
- return `assets/${filename}`;
102
+ return `assets/${filename}${suffix}`;
88
103
  }
89
104
  export { resolveAssetSrc, resolveContent };