specra 0.2.66 → 0.2.67

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 (2) hide show
  1. package/dist/mdx.js +113 -15
  2. package/package.json +2 -1
package/dist/mdx.js CHANGED
@@ -10,6 +10,7 @@ import remarkGfm from "remark-gfm";
10
10
  import remarkMath from "remark-math";
11
11
  import remarkRehype from "remark-rehype";
12
12
  import rehypeSlug from "rehype-slug";
13
+ import { slug as githubSlug } from "github-slugger";
13
14
  import rehypeRaw from "rehype-raw";
14
15
  import rehypeKatex from "rehype-katex";
15
16
  import rehypeStringify from "rehype-stringify";
@@ -170,11 +171,92 @@ function maskInlineCodePipes(markdown) {
170
171
  }).join('');
171
172
  }
172
173
  /**
173
- * Restore `PIPE_MARKER` back to `|` in a string. No-op if the marker
174
- * isn't present (the common case), so cheap to call blanket.
174
+ * Private Use Area characters used to mask `<` and `>` written inside code
175
+ * (inline spans or fenced blocks) that sits within a component block.
176
+ *
177
+ * Why: `ensureComponentBlockIntegrity` deliberately collapses blank lines so
178
+ * that an entire `<Callout>…</Callout>` stays one CommonMark HTML block. But
179
+ * inside an HTML block, markdown no longer applies — code spans and fences stop
180
+ * being code — so a documented tag such as
181
+ * An inline `<script src="/x.js">` tag.
182
+ * is handed to rehype-raw as *real HTML*. parse5 opens a raw-text `<script>`
183
+ * element that is never closed and swallows every following sibling, so the
184
+ * next heading silently disappears from the page and its ToC link 404s.
185
+ *
186
+ * Masking the angle brackets before remark sees them keeps the block inert for
187
+ * parse5, and `restoreCodeMarkers` puts them back after the component's
188
+ * children have been re-parsed as markdown. PUA chars pass through
189
+ * remark/rehype unchanged and are never HTML-escaped — same trick as
190
+ * `PIPE_MARKER` above.
191
+ */
192
+ const LT_MARKER = '';
193
+ const GT_MARKER = '';
194
+ /**
195
+ * Mask `<` and `>` inside fenced code blocks and single-line inline code spans.
196
+ * Applied only to the interior of component blocks, where markdown code markers
197
+ * lose their meaning. Component tags themselves live outside code and are left
198
+ * alone, so `<Callout>` still parses as a component.
199
+ */
200
+ function maskCodeAngleBrackets(markdown) {
201
+ const maskAngles = (s) => s.replace(/</g, LT_MARKER).replace(/>/g, GT_MARKER);
202
+ return splitByCodeFences(markdown).map(({ text, isCode }) => {
203
+ if (isCode)
204
+ return maskAngles(text);
205
+ let result = '';
206
+ let i = 0;
207
+ while (i < text.length) {
208
+ if (text[i] !== '`') {
209
+ result += text[i];
210
+ i++;
211
+ continue;
212
+ }
213
+ let openLen = 0;
214
+ while (i + openLen < text.length && text[i + openLen] === '`')
215
+ openLen++;
216
+ let j = i + openLen;
217
+ let closeIdx = -1;
218
+ while (j < text.length && text[j] !== '\n') {
219
+ if (text[j] === '`') {
220
+ let closeLen = 0;
221
+ while (j + closeLen < text.length && text[j + closeLen] === '`')
222
+ closeLen++;
223
+ if (closeLen === openLen) {
224
+ closeIdx = j;
225
+ break;
226
+ }
227
+ j += closeLen;
228
+ }
229
+ else {
230
+ j++;
231
+ }
232
+ }
233
+ if (closeIdx === -1) {
234
+ result += text.slice(i, i + openLen);
235
+ i += openLen;
236
+ }
237
+ else {
238
+ const content = maskAngles(text.slice(i + openLen, closeIdx));
239
+ result += text.slice(i, i + openLen) + content + text.slice(closeIdx, closeIdx + openLen);
240
+ i = closeIdx + openLen;
241
+ }
242
+ }
243
+ return result;
244
+ }).join('');
245
+ }
246
+ /**
247
+ * Restore `PIPE_MARKER` back to `|`, and `LT_MARKER`/`GT_MARKER` back to
248
+ * `<`/`>`, in a string. No-op if no marker is present (the common case), so
249
+ * cheap to call blanket.
175
250
  */
176
251
  function restorePipeMarkers(s) {
177
- return s.indexOf(PIPE_MARKER) === -1 ? s : s.split(PIPE_MARKER).join('|');
252
+ let out = s;
253
+ if (out.indexOf(PIPE_MARKER) !== -1)
254
+ out = out.split(PIPE_MARKER).join('|');
255
+ if (out.indexOf(LT_MARKER) !== -1)
256
+ out = out.split(LT_MARKER).join('<');
257
+ if (out.indexOf(GT_MARKER) !== -1)
258
+ out = out.split(GT_MARKER).join('>');
259
+ return out;
178
260
  }
179
261
  /**
180
262
  * Walk an MdxNode tree and restore `PIPE_MARKER` to `|` in every string
@@ -1168,7 +1250,14 @@ function ensureComponentBlockIntegrity(markdown) {
1168
1250
  // code fences. The code fence content is raw text inside the HTML block and
1169
1251
  // will be re-processed by processComponentChildren through the markdown
1170
1252
  // pipeline, which restores proper code formatting.
1171
- const collapsed = block.replace(/^\s*$/gm, '<!-- -->');
1253
+ // Mask `<`/`>` written inside code (inline spans and fences) BEFORE the
1254
+ // blank-line collapse turns this whole block into one raw HTML block.
1255
+ // Once it is raw HTML, markdown code markers no longer protect their
1256
+ // contents, and a documented tag like `<script src="...">` reaches parse5
1257
+ // as real HTML — a raw-text element that never closes and swallows every
1258
+ // sibling after the component, heading and all. Restored by
1259
+ // `restorePipeMarkers` once the children are re-parsed as markdown.
1260
+ const collapsed = maskCodeAngleBrackets(block).replace(/^\s*$/gm, '<!-- -->');
1172
1261
  result += markdown.slice(lastIndex, blockStart) + collapsed;
1173
1262
  lastIndex = blockEnd;
1174
1263
  openTagRegex.lastIndex = blockEnd;
@@ -1623,17 +1712,26 @@ export function getAdjacentDocs(currentSlug, allDocs) {
1623
1712
  export function extractTableOfContents(content) {
1624
1713
  const headingRegex = /^(#{2,3})\s+(.+)$/gm;
1625
1714
  const toc = [];
1626
- let match;
1627
- while ((match = headingRegex.exec(content)) !== null) {
1628
- const level = match[1].length;
1629
- const text = match[2];
1630
- // Generate ID the same way rehype-slug does
1631
- const id = text
1632
- .toLowerCase()
1633
- .replace(/\s+/g, "-") // Replace spaces with hyphens first
1634
- .replace(/[^a-z0-9-]/g, "") // Remove special chars (dots, slashes, etc)
1635
- .replace(/^-|-$/g, ""); // Remove leading/trailing hyphens
1636
- toc.push({ id, title: text, level });
1715
+ // Headings inside fenced code blocks are code, not headings. They get no
1716
+ // `id` in the rendered page, so a ToC entry for them can only ever 404.
1717
+ for (const { text: segment, isCode } of splitByCodeFences(content)) {
1718
+ if (isCode)
1719
+ continue;
1720
+ let match;
1721
+ headingRegex.lastIndex = 0;
1722
+ while ((match = headingRegex.exec(segment)) !== null) {
1723
+ const level = match[1].length;
1724
+ const text = match[2];
1725
+ // Use the SAME slugger rehype-slug uses, rather than approximating it.
1726
+ // The old hand-rolled version stripped `_` and trimmed a trailing `-`,
1727
+ // while github-slugger keeps both — so every heading naming a snake_case
1728
+ // identifier (`list_display`, `on_upload`, `cache_page`) produced a ToC
1729
+ // link that pointed at nothing. Backticks are stripped first because
1730
+ // rehype-slug slugs the RENDERED heading text, where `code` markup is
1731
+ // already gone.
1732
+ const id = githubSlug(text.replace(/`/g, ""));
1733
+ toc.push({ id, title: text, level });
1734
+ }
1637
1735
  }
1638
1736
  return toc;
1639
1737
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specra",
3
- "version": "0.2.66",
3
+ "version": "0.2.67",
4
4
  "description": "A modern documentation library for SvelteKit with built-in versioning, API reference generation, full-text search, and MDX support",
5
5
  "svelte": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -81,6 +81,7 @@
81
81
  "clsx": "^2.1.1",
82
82
  "date-fns": "^4.1.0",
83
83
  "embla-carousel-svelte": "^8.5.1",
84
+ "github-slugger": "^2.0.0",
84
85
  "gray-matter": "^4.0.3",
85
86
  "hast-util-to-html": "^9.0.0",
86
87
  "js-yaml": "^4.1.1",