specra 0.2.58 → 0.2.60
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/dist/components/docs/CodeBlock.svelte +12 -2
- package/dist/components/docs/CodeBlock.svelte.d.ts +1 -0
- package/dist/mdx-security.d.ts +13 -2
- package/dist/mdx-security.js +0 -0
- package/dist/mdx.js +17 -3
- package/dist/remark-code-meta.d.ts +11 -3
- package/dist/remark-code-meta.js +29 -7
- package/package.json +1 -1
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
code: string;
|
|
6
6
|
language: string;
|
|
7
7
|
filename?: string;
|
|
8
|
+
showLineNumbers?: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
let { code, language, filename }: Props = $props();
|
|
11
|
+
let { code, language, filename, showLineNumbers = false }: Props = $props();
|
|
12
|
+
|
|
13
|
+
// Languages whose syntax the JS/TS-oriented tokenizer below does NOT model.
|
|
14
|
+
// Highlighting these would mis-colour prose markup (e.g. a markdown list `-`
|
|
15
|
+
// becomes a red "operator", `# heading` becomes a grey "comment"), so they are
|
|
16
|
+
// rendered as plain, uncoloured text instead.
|
|
17
|
+
const PLAIN_LANGUAGES = new Set([
|
|
18
|
+
'markdown', 'md', 'mdx', 'text', 'txt', 'plaintext', 'plain', '',
|
|
19
|
+
]);
|
|
20
|
+
let isPlain = $derived(PLAIN_LANGUAGES.has((language || '').toLowerCase()));
|
|
11
21
|
|
|
12
22
|
let copied = $state(false);
|
|
13
23
|
let copyTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -115,7 +125,7 @@
|
|
|
115
125
|
|
|
116
126
|
<!-- Code content -->
|
|
117
127
|
<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}"
|
|
128
|
+
<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 = 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 line}{line}{:else} {/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
129
|
</div>
|
|
120
130
|
</div>
|
|
121
131
|
|
package/dist/mdx-security.d.ts
CHANGED
|
@@ -23,8 +23,19 @@ export declare function validatePathWithinDirectory(filePath: string, allowedDir
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function scanMDXForDangerousPatterns(content: string): string[];
|
|
25
25
|
/**
|
|
26
|
-
* Sanitize MDX content by
|
|
27
|
-
*
|
|
26
|
+
* Sanitize MDX content by neutralizing dangerous patterns in executable prose,
|
|
27
|
+
* while leaving fenced code blocks and inline code completely untouched.
|
|
28
|
+
*
|
|
29
|
+
* Code spans are not executable — the code renderer displays their contents as
|
|
30
|
+
* escaped text — so dangerous-looking examples inside them (e.g. a documented
|
|
31
|
+
* `<script>` tag) must be preserved verbatim, never stripped. We mask code spans
|
|
32
|
+
* with placeholder tokens, sanitize only the surrounding prose, then restore
|
|
33
|
+
* them unchanged.
|
|
34
|
+
*
|
|
35
|
+
* In prose, script tags are *escaped* (turned into inert, visible literal text)
|
|
36
|
+
* rather than deleted, so content never silently vanishes. Event-handler
|
|
37
|
+
* attributes and `javascript:` URLs are invisible attack vectors on otherwise
|
|
38
|
+
* valid HTML, so those are removed (neutralizing them deletes no visible text).
|
|
28
39
|
*/
|
|
29
40
|
export declare function sanitizeMDXContent(content: string, strict?: boolean): string;
|
|
30
41
|
/**
|
package/dist/mdx-security.js
CHANGED
|
Binary file
|
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,20 @@ 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
|
+
return {
|
|
589
|
+
code,
|
|
590
|
+
language,
|
|
591
|
+
...(filename ? { filename } : {}),
|
|
592
|
+
...(showLineNumbers ? { showLineNumbers: true } : {}),
|
|
593
|
+
};
|
|
581
594
|
}
|
|
582
595
|
/**
|
|
583
596
|
* Detect GitHub-style alert blockquotes: > [!WARNING] content
|
|
@@ -1176,6 +1189,7 @@ async function processMarkdownToMdxNodes(markdown) {
|
|
|
1176
1189
|
.use(remarkParse)
|
|
1177
1190
|
.use(remarkGfm)
|
|
1178
1191
|
.use(remarkMath)
|
|
1192
|
+
.use(remarkCodeMeta)
|
|
1179
1193
|
.use(remarkRehype, { allowDangerousHtml: true })
|
|
1180
1194
|
.use(rehypeRaw)
|
|
1181
1195
|
.use(rehypeSlug)
|
|
@@ -1,6 +1,14 @@
|
|
|
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
|
+
*
|
|
11
|
+
* Line numbers are opt-in: only an explicit `showLineNumbers` (or
|
|
12
|
+
* `showLineNumbers=true`) turns them on; absence (or `=false`) leaves them off.
|
|
5
13
|
*/
|
|
6
14
|
export declare function remarkCodeMeta(): (tree: any) => void;
|
package/dist/remark-code-meta.js
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
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
|
+
*
|
|
11
|
+
* Line numbers are opt-in: only an explicit `showLineNumbers` (or
|
|
12
|
+
* `showLineNumbers=true`) turns them on; absence (or `=false`) leaves them off.
|
|
5
13
|
*/
|
|
6
14
|
export function remarkCodeMeta() {
|
|
7
15
|
return (tree) => {
|
|
8
16
|
const visit = (node) => {
|
|
9
17
|
if (node.type === 'code' && node.meta) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
const hProperties = {};
|
|
19
|
+
// title="..." / title='...' -> filename shown in the code block header.
|
|
20
|
+
const titleMatch = node.meta.match(/\btitle\s*=\s*"([^"]*)"/i) ||
|
|
21
|
+
node.meta.match(/\btitle\s*=\s*'([^']*)'/i);
|
|
22
|
+
if (titleMatch) {
|
|
23
|
+
hProperties['data-filename'] = titleMatch[1];
|
|
24
|
+
}
|
|
25
|
+
// showLineNumbers (opt-in). Bare flag or `=true` enables; `=false` keeps off.
|
|
26
|
+
const lineNumbersMatch = node.meta.match(/\bshowLineNumbers(?:\s*=\s*(true|false))?\b/i);
|
|
27
|
+
if (lineNumbersMatch &&
|
|
28
|
+
(lineNumbersMatch[1] === undefined ||
|
|
29
|
+
lineNumbersMatch[1].toLowerCase() === 'true')) {
|
|
30
|
+
hProperties['data-line-numbers'] = 'true';
|
|
31
|
+
}
|
|
32
|
+
if (Object.keys(hProperties).length > 0) {
|
|
33
|
+
node.data = node.data || {};
|
|
34
|
+
node.data.hProperties = { ...(node.data.hProperties || {}), ...hProperties };
|
|
35
|
+
}
|
|
14
36
|
}
|
|
15
37
|
if (node.children) {
|
|
16
38
|
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.60",
|
|
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",
|