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 =
|
|
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} {/if}{/if}{:else}{#if tokens.length === 0} {: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
|
|
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
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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;
|
package/dist/remark-code-meta.js
CHANGED
|
@@ -1,16 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Remark plugin to
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
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",
|