specra 0.2.59 → 0.2.61

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.
@@ -5,9 +5,24 @@
5
5
  code: string;
6
6
  language: string;
7
7
  filename?: string;
8
+ showLineNumbers?: boolean;
9
+ diff?: boolean;
8
10
  }
9
11
 
10
- let { code, language, filename }: Props = $props();
12
+ let { code, language, filename, showLineNumbers = false, diff = false }: Props = $props();
13
+
14
+ // Diff add/remove markers apply either to a `diff` language or to any block
15
+ // explicitly opted in via the `diff` fence-meta flag.
16
+ let diffEnabled = $derived(diff || language === 'diff');
17
+
18
+ // Languages whose syntax the JS/TS-oriented tokenizer below does NOT model.
19
+ // Highlighting these would mis-colour prose markup (e.g. a markdown list `-`
20
+ // becomes a red "operator", `# heading` becomes a grey "comment"), so they are
21
+ // rendered as plain, uncoloured text instead.
22
+ const PLAIN_LANGUAGES = new Set([
23
+ 'markdown', 'md', 'mdx', 'text', 'txt', 'plaintext', 'plain', '',
24
+ ]);
25
+ let isPlain = $derived(PLAIN_LANGUAGES.has((language || '').toLowerCase()));
11
26
 
12
27
  let copied = $state(false);
13
28
  let copyTimeout: ReturnType<typeof setTimeout> | undefined;
@@ -115,7 +130,7 @@
115
130
 
116
131
  <!-- Code content -->
117
132
  <div class="bg-muted/30 dark:bg-muted/20 rounded-b-xl overflow-x-auto border border-border/50">
118
- <pre class="p-2 text-[13px] font-mono leading-relaxed text-gray-800 dark:text-gray-200"><code class="table w-full">{#each lines as line, i}{@const isDeletion = language === 'diff' && line.startsWith('-') && !line.startsWith('--')}{@const isAddition = language === 'diff' && line.startsWith('+') && !line.startsWith('++')}{@const isDiff = isDeletion || isAddition}{@const diffBgClass = isDeletion ? 'bg-red-500/5 dark:bg-red-500/10' : isAddition ? 'bg-green-500/5 dark:bg-green-500/10' : ''}{@const diffMarkerClass = isDeletion ? 'text-red-600 dark:text-red-400' : isAddition ? 'text-green-600 dark:text-green-400' : ''}{@const tokens = tokenizeLine(line)}<div class="table-row {diffBgClass}"><span class="table-cell pr-4 text-right select-none text-muted-foreground/40 w-8 align-top">{i + 1}</span><span class="table-cell align-top">{#if tokens.length === 0}&nbsp;{:else}{#each tokens as token, j}{#if j === 0 && isDiff && token.value.length > 0 && (token.value[0] === '+' || token.value[0] === '-')}<span class="{diffMarkerClass} font-bold">{token.value[0]}</span>{#if token.value.length > 1}<span class="token-{token.type}">{token.value.slice(1)}</span>{/if}{:else}<span class="token-{token.type}">{token.value}</span>{/if}{/each}{/if}</span></div>{/each}</code></pre>
133
+ <pre class="p-2 text-[13px] font-mono leading-relaxed text-gray-800 dark:text-gray-200"><code class="table w-full">{#each lines as line, i}{@const isDeletion = diffEnabled && line.startsWith('-') && !line.startsWith('--')}{@const isAddition = diffEnabled && line.startsWith('+') && !line.startsWith('++')}{@const isDiff = isDeletion || isAddition}{@const diffBgClass = isDeletion ? 'bg-red-500/5 dark:bg-red-500/10' : isAddition ? 'bg-green-500/5 dark:bg-green-500/10' : ''}{@const diffMarkerClass = isDeletion ? 'text-red-600 dark:text-red-400' : isAddition ? 'text-green-600 dark:text-green-400' : ''}{@const tokens = isPlain ? [] : tokenizeLine(line)}<div class="table-row {diffBgClass}">{#if showLineNumbers}<span class="table-cell pr-4 text-right select-none text-muted-foreground/40 w-8 align-top">{i + 1}</span>{/if}<span class="table-cell align-top">{#if isPlain}{#if isDiff}<span class="{diffMarkerClass} font-bold">{line[0]}</span>{line.slice(1)}{:else}{#if line}{line}{:else}&nbsp;{/if}{/if}{:else}{#if tokens.length === 0}&nbsp;{:else}{#each tokens as token, j}{#if j === 0 && isDiff && token.value.length > 0 && (token.value[0] === '+' || token.value[0] === '-')}<span class="{diffMarkerClass} font-bold">{token.value[0]}</span>{#if token.value.length > 1}<span class="token-{token.type}">{token.value.slice(1)}</span>{/if}{:else}<span class="token-{token.type}">{token.value}</span>{/if}{/each}{/if}{/if}</span></div>{/each}</code></pre>
119
134
  </div>
120
135
  </div>
121
136
 
@@ -2,6 +2,8 @@ interface Props {
2
2
  code: string;
3
3
  language: string;
4
4
  filename?: string;
5
+ showLineNumbers?: boolean;
6
+ diff?: boolean;
5
7
  }
6
8
  declare const CodeBlock: import("svelte").Component<Props, {}, "">;
7
9
  type CodeBlock = ReturnType<typeof CodeBlock>;
package/dist/mdx.js CHANGED
@@ -3,6 +3,7 @@ import path from "path";
3
3
  import matter from "gray-matter";
4
4
  import yaml from "js-yaml";
5
5
  import { rehypeBasePath } from "./rehype-base-path.js";
6
+ import { remarkCodeMeta } from "./remark-code-meta.js";
6
7
  import { unified } from "unified";
7
8
  import remarkParse from "remark-parse";
8
9
  import remarkGfm from "remark-gfm";
@@ -518,6 +519,7 @@ async function processMarkdownToHtml(markdown) {
518
519
  .use(remarkParse)
519
520
  .use(remarkGfm)
520
521
  .use(remarkMath)
522
+ .use(remarkCodeMeta)
521
523
  .use(remarkRehype, { allowDangerousHtml: true })
522
524
  .use(rehypeRaw)
523
525
  .use(rehypeSlug)
@@ -575,9 +577,23 @@ function extractCodeBlockProps(node) {
575
577
  const language = langClass ? langClass.replace('language-', '') : 'txt';
576
578
  // Extract text content from the code element
577
579
  const code = extractTextContent(codeChild).replace(/\n$/, '');
578
- // Check for filename in data attributes (e.g. from remark-code-meta)
579
- const filename = node.properties?.['data-filename'] || codeChild.properties?.['data-filename'];
580
- return { code, language, ...(filename ? { filename } : {}) };
580
+ // Read attributes set by remark-code-meta from the fence meta. rehypeRaw
581
+ // normalises `data-foo` to the camel-cased `dataFoo` hast property, so accept
582
+ // both spellings, on either the <pre> or the <code>.
583
+ const readDataAttr = (camel, kebab) => node.properties?.[camel] ?? node.properties?.[kebab] ??
584
+ codeChild.properties?.[camel] ?? codeChild.properties?.[kebab];
585
+ const filename = readDataAttr('dataFilename', 'data-filename');
586
+ const lineNumbers = readDataAttr('dataLineNumbers', 'data-line-numbers');
587
+ const showLineNumbers = lineNumbers === 'true' || lineNumbers === true || lineNumbers === '';
588
+ const diffAttr = readDataAttr('dataDiff', 'data-diff');
589
+ const diff = diffAttr === 'true' || diffAttr === true || diffAttr === '';
590
+ return {
591
+ code,
592
+ language,
593
+ ...(filename ? { filename } : {}),
594
+ ...(showLineNumbers ? { showLineNumbers: true } : {}),
595
+ ...(diff ? { diff: true } : {}),
596
+ };
581
597
  }
582
598
  /**
583
599
  * Detect GitHub-style alert blockquotes: > [!WARNING] content
@@ -1176,6 +1192,7 @@ async function processMarkdownToMdxNodes(markdown) {
1176
1192
  .use(remarkParse)
1177
1193
  .use(remarkGfm)
1178
1194
  .use(remarkMath)
1195
+ .use(remarkCodeMeta)
1179
1196
  .use(remarkRehype, { allowDangerousHtml: true })
1180
1197
  .use(rehypeRaw)
1181
1198
  .use(rehypeSlug)
@@ -1,6 +1,16 @@
1
1
  /**
2
- * Remark plugin to extract code block meta strings and pass them as props
3
- * Converts: ```js filename.js
4
- * Into props: { language: 'js', meta: 'filename.js' }
2
+ * Remark plugin to read a code block's fence meta string and expose the
3
+ * recognised options as `data-*` attributes on the rendered <code>.
4
+ *
5
+ * Supported syntax:
6
+ * ```js title="app.js" -> data-filename="app.js"
7
+ * ```ts title='src/index.ts' -> data-filename="src/index.ts"
8
+ * ```js showLineNumbers -> data-line-numbers="true"
9
+ * ```js showLineNumbers=true -> data-line-numbers="true"
10
+ * ```html diff -> data-diff="true"
11
+ *
12
+ * Line numbers and diff markers are opt-in: only an explicit flag turns them on.
13
+ * `diff` lets a syntax-highlighted block (any language) render leading `-`/`+`
14
+ * lines as removed/added without auto-detecting them across every language.
5
15
  */
6
16
  export declare function remarkCodeMeta(): (tree: any) => void;
@@ -1,16 +1,50 @@
1
1
  /**
2
- * Remark plugin to extract code block meta strings and pass them as props
3
- * Converts: ```js filename.js
4
- * Into props: { language: 'js', meta: 'filename.js' }
2
+ * Remark plugin to read a code block's fence meta string and expose the
3
+ * recognised options as `data-*` attributes on the rendered <code>.
4
+ *
5
+ * Supported syntax:
6
+ * ```js title="app.js" -> data-filename="app.js"
7
+ * ```ts title='src/index.ts' -> data-filename="src/index.ts"
8
+ * ```js showLineNumbers -> data-line-numbers="true"
9
+ * ```js showLineNumbers=true -> data-line-numbers="true"
10
+ * ```html diff -> data-diff="true"
11
+ *
12
+ * Line numbers and diff markers are opt-in: only an explicit flag turns them on.
13
+ * `diff` lets a syntax-highlighted block (any language) render leading `-`/`+`
14
+ * lines as removed/added without auto-detecting them across every language.
5
15
  */
6
16
  export function remarkCodeMeta() {
7
17
  return (tree) => {
8
18
  const visit = (node) => {
9
19
  if (node.type === 'code' && node.meta) {
10
- // Store the meta string in the node's data
11
- node.data = node.data || {};
12
- node.data.hProperties = node.data.hProperties || {};
13
- node.data.hProperties.meta = node.meta;
20
+ const meta = node.meta;
21
+ const hProperties = {};
22
+ // title="..." / title='...' -> filename shown in the code block header.
23
+ const titleMatch = meta.match(/\btitle\s*=\s*"([^"]*)"/i) ||
24
+ meta.match(/\btitle\s*=\s*'([^']*)'/i);
25
+ if (titleMatch) {
26
+ hProperties['data-filename'] = titleMatch[1];
27
+ }
28
+ // Strip quoted `key="..."` / `key='...'` values before scanning for bare
29
+ // flags, so a value like title="my diff notes" can't false-match below.
30
+ const flags = meta
31
+ .replace(/\b\w+\s*=\s*"[^"]*"/g, ' ')
32
+ .replace(/\b\w+\s*=\s*'[^']*'/g, ' ');
33
+ // showLineNumbers (opt-in). Bare flag or `=true` enables; `=false` keeps off.
34
+ const lineNumbersMatch = flags.match(/\bshowLineNumbers(?:\s*=\s*(true|false))?\b/i);
35
+ if (lineNumbersMatch &&
36
+ (lineNumbersMatch[1] === undefined ||
37
+ lineNumbersMatch[1].toLowerCase() === 'true')) {
38
+ hProperties['data-line-numbers'] = 'true';
39
+ }
40
+ // diff (opt-in): treat leading -/+ lines as removed/added.
41
+ if (/\bdiff\b/i.test(flags)) {
42
+ hProperties['data-diff'] = 'true';
43
+ }
44
+ if (Object.keys(hProperties).length > 0) {
45
+ node.data = node.data || {};
46
+ node.data.hProperties = { ...(node.data.hProperties || {}), ...hProperties };
47
+ }
14
48
  }
15
49
  if (node.children) {
16
50
  node.children.forEach(visit);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specra",
3
- "version": "0.2.59",
3
+ "version": "0.2.61",
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",