@mitcsutt/kiln-ui 0.5.0 → 0.6.0
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/display/CodeBlock/CodeBlock.d.ts +44 -3
- package/dist/components/display/CodeBlock/CodeBlock.js +54 -49
- package/dist/components/display/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/display/CodeBlock/CodeBlock.module.js +4 -3
- package/dist/components/display/CodeBlock/CodeBlock.module.js.map +1 -1
- package/dist/components/display/CodeBlock/index.d.ts +1 -1
- package/dist/highlight/index.d.ts +18 -0
- package/dist/highlight/index.js +103 -0
- package/dist/highlight/index.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/newlines.d.ts +2 -0
- package/dist/utils/newlines.js +10 -0
- package/dist/utils/newlines.js.map +1 -0
- package/package.json +13 -2
- package/skills/setup-and-theming/SKILL.md +1 -0
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* What a piece of highlighted source is. Each kind is coloured from the theme's own tokens, so
|
|
4
|
+
* highlighted code follows the theme and the colour mode.
|
|
5
|
+
*/
|
|
6
|
+
export type CodeTokenType = 'keyword' | 'string' | 'comment' | 'constant' | 'function' | 'type' | 'tag' | 'attribute' | 'punctuation';
|
|
7
|
+
/** A run of source text and its kind. A token with no `type` is plain text. */
|
|
8
|
+
export interface CodeToken {
|
|
9
|
+
content: string;
|
|
10
|
+
type?: CodeTokenType;
|
|
11
|
+
}
|
|
2
12
|
export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|
3
13
|
/** The source, verbatim. Trailing newlines are trimmed. */
|
|
4
14
|
code: string;
|
|
5
|
-
/** Shown as a label ("TypeScript", "bash").
|
|
15
|
+
/** Shown as a label ("TypeScript", "bash"). It doesn't turn on highlighting: `tokens` does. */
|
|
6
16
|
language?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Syntax highlighting for `code`: one array of tokens per line, as `highlight` from
|
|
19
|
+
* `@mitcsutt/kiln-ui/highlight` returns them. Without it, or when the tokens don't spell out
|
|
20
|
+
* `code` line for line (say, tokens from a previous `code`), the code is plain.
|
|
21
|
+
*/
|
|
22
|
+
tokens?: CodeToken[][];
|
|
7
23
|
/** Filename or caption shown in the header: `src/queries/keys.ts`. */
|
|
8
24
|
title?: ReactNode;
|
|
9
25
|
/** Number the lines in a gutter (not copied with a selection). */
|
|
@@ -23,8 +39,33 @@ export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLElement>, 'title
|
|
|
23
39
|
*
|
|
24
40
|
* @remarks
|
|
25
41
|
* `CodeBlock` shows code: a snippet in an article, a command, a config example. It's a plain
|
|
26
|
-
* surface with an optional title bar, never fake window chrome
|
|
27
|
-
*
|
|
42
|
+
* surface with an optional title bar, never fake window chrome. It's plain unless you pass
|
|
43
|
+
* `tokens`: `highlight` from `@mitcsutt/kiln-ui/highlight` turns JavaScript, JSX, TypeScript and
|
|
44
|
+
* TSX into tokens, coloured from the theme. Every code sample in these docs is a `CodeBlock`.
|
|
45
|
+
*
|
|
46
|
+
* ## Highlighting
|
|
47
|
+
*
|
|
48
|
+
* `highlight(code, language)` resolves to the tokens for `code`, or to `undefined` for a language
|
|
49
|
+
* other than `js`, `jsx`, `ts` or `tsx` (or `javascript` and `typescript`), which `CodeBlock`
|
|
50
|
+
* shows as plain code. It runs [Shiki](https://shiki.style), an optional peer dependency, so
|
|
51
|
+
* install `shiki` to use it. The main `@mitcsutt/kiln-ui` entry never loads it.
|
|
52
|
+
*
|
|
53
|
+
* Keywords and tags take the accent, strings, constants, functions and types take the theme's
|
|
54
|
+
* tones, and comments and punctuation are muted. A custom theme needs no code palette of its own.
|
|
55
|
+
*
|
|
56
|
+
* The tokens are plain data, so highlight wherever the code is ready. In a server component or
|
|
57
|
+
* at build time, `await` it and pass the tokens to the client component that renders the
|
|
58
|
+
* `CodeBlock`, and the browser downloads no highlighter. In the browser, call it in an effect, as
|
|
59
|
+
* in the example above: Shiki and the grammar load on the first call.
|
|
60
|
+
*
|
|
61
|
+
* ```tsx
|
|
62
|
+
* import { highlight } from '@mitcsutt/kiln-ui/highlight'
|
|
63
|
+
*
|
|
64
|
+
* export async function Snippet({ source }: { source: string }) {
|
|
65
|
+
* const tokens = await highlight(source, 'tsx')
|
|
66
|
+
* return <SnippetBlock code={source} tokens={tokens} />
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
28
69
|
*
|
|
29
70
|
* @privateRemarks
|
|
30
71
|
* A block of source: a snippet in a blog post, a command in a README, a config example.
|
|
@@ -1,85 +1,90 @@
|
|
|
1
1
|
import { cx as e } from "../../../utils/cx.js";
|
|
2
2
|
import { CheckIcon as t, CopyIcon as n } from "../../../icons/icons.js";
|
|
3
3
|
import { Button as r } from "../../actions/Button/Button.js";
|
|
4
|
-
import i from "
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import { trimTrailingNewlines as i } from "../../../utils/newlines.js";
|
|
5
|
+
import a from "./CodeBlock.module.js";
|
|
6
|
+
import { forwardRef as o, useEffect as s, useId as c, useRef as l, useState as u } from "react";
|
|
7
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
7
8
|
//#region src/components/display/CodeBlock/CodeBlock.tsx
|
|
8
|
-
var
|
|
9
|
-
let [
|
|
10
|
-
|
|
11
|
-
clearTimeout(
|
|
9
|
+
var p = o(function({ code: o, language: p, tokens: m, title: h, showLineNumbers: g = !1, highlightLines: _, copyable: v = !0, wrap: y = !1, copyLabel: b = "Copy code", className: x, ...S }, C) {
|
|
10
|
+
let [w, T] = u("idle"), E = l(void 0), D = c(), O = i(o), k = O.split("\n"), A = m?.length === k.length && m.every((e, t) => e.map((e) => e.content).join("") === k[t]) ? m : void 0, j = new Set(_), M = !!h || !!p || v;
|
|
11
|
+
s(() => () => {
|
|
12
|
+
clearTimeout(E.current);
|
|
12
13
|
}, []);
|
|
13
|
-
let
|
|
14
|
-
clearTimeout(
|
|
14
|
+
let N = async () => {
|
|
15
|
+
clearTimeout(E.current);
|
|
15
16
|
try {
|
|
16
|
-
await navigator.clipboard.writeText(
|
|
17
|
+
await navigator.clipboard.writeText(O), T("copied");
|
|
17
18
|
} catch {
|
|
18
|
-
|
|
19
|
+
T("failed");
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
E.current = setTimeout(() => {
|
|
22
|
+
T("idle");
|
|
22
23
|
}, 2e3);
|
|
23
24
|
};
|
|
24
|
-
return /* @__PURE__ */
|
|
25
|
-
ref:
|
|
26
|
-
className: e(
|
|
25
|
+
return /* @__PURE__ */ f("figure", {
|
|
26
|
+
ref: C,
|
|
27
|
+
className: e(a.codeBlock, x),
|
|
27
28
|
"data-kiln-component": "",
|
|
28
|
-
"data-wrap":
|
|
29
|
-
"data-line-numbers":
|
|
30
|
-
"aria-labelledby":
|
|
31
|
-
...
|
|
29
|
+
"data-wrap": y || void 0,
|
|
30
|
+
"data-line-numbers": g || void 0,
|
|
31
|
+
"aria-labelledby": h ? D : void 0,
|
|
32
|
+
...S,
|
|
32
33
|
children: [
|
|
33
|
-
|
|
34
|
-
className:
|
|
34
|
+
M ? /* @__PURE__ */ f("figcaption", {
|
|
35
|
+
className: a.header,
|
|
35
36
|
children: [
|
|
36
|
-
|
|
37
|
-
id:
|
|
38
|
-
className:
|
|
39
|
-
children:
|
|
37
|
+
h ? /* @__PURE__ */ d("span", {
|
|
38
|
+
id: D,
|
|
39
|
+
className: a.title,
|
|
40
|
+
children: h
|
|
40
41
|
}) : null,
|
|
41
|
-
|
|
42
|
-
className:
|
|
43
|
-
children:
|
|
42
|
+
p ? /* @__PURE__ */ d("span", {
|
|
43
|
+
className: a.language,
|
|
44
|
+
children: p
|
|
44
45
|
}) : null,
|
|
45
|
-
|
|
46
|
+
v ? /* @__PURE__ */ d(r, {
|
|
46
47
|
variant: "ghost",
|
|
47
48
|
tone: "neutral",
|
|
48
49
|
size: "sm",
|
|
49
|
-
className:
|
|
50
|
-
leadingIcon:
|
|
51
|
-
"aria-label":
|
|
50
|
+
className: a.copy,
|
|
51
|
+
leadingIcon: d(w === "copied" ? t : n, {}),
|
|
52
|
+
"aria-label": b,
|
|
52
53
|
onClick: () => {
|
|
53
|
-
|
|
54
|
+
N();
|
|
54
55
|
},
|
|
55
|
-
"data-state":
|
|
56
|
-
children:
|
|
56
|
+
"data-state": w,
|
|
57
|
+
children: w === "copied" ? "Copied" : w === "failed" ? "Copy failed" : "Copy"
|
|
57
58
|
}) : null
|
|
58
59
|
]
|
|
59
60
|
}) : null,
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
-
className:
|
|
61
|
+
/* @__PURE__ */ d("pre", {
|
|
62
|
+
className: a.pre,
|
|
62
63
|
tabIndex: 0,
|
|
63
|
-
children: /* @__PURE__ */
|
|
64
|
-
className:
|
|
65
|
-
children:
|
|
66
|
-
className:
|
|
67
|
-
"data-line":
|
|
68
|
-
"data-highlighted":
|
|
69
|
-
children: [e, t
|
|
64
|
+
children: /* @__PURE__ */ d("code", {
|
|
65
|
+
className: a.code,
|
|
66
|
+
children: k.map((e, t) => /* @__PURE__ */ f("span", {
|
|
67
|
+
className: a.line,
|
|
68
|
+
"data-line": g ? t + 1 : void 0,
|
|
69
|
+
"data-highlighted": j.has(t + 1) || void 0,
|
|
70
|
+
children: [A?.[t] ? A[t].map((e, t) => e.type ? /* @__PURE__ */ d("span", {
|
|
71
|
+
className: a.token,
|
|
72
|
+
"data-token": e.type,
|
|
73
|
+
children: e.content
|
|
74
|
+
}, t) : e.content) : e, t < k.length - 1 ? "\n" : null]
|
|
70
75
|
}, t))
|
|
71
76
|
})
|
|
72
77
|
}),
|
|
73
|
-
|
|
74
|
-
className:
|
|
78
|
+
v ? /* @__PURE__ */ d("span", {
|
|
79
|
+
className: a.visuallyHidden,
|
|
75
80
|
role: "status",
|
|
76
81
|
"aria-live": "polite",
|
|
77
|
-
children:
|
|
82
|
+
children: w === "copied" ? "Copied to clipboard" : w === "failed" ? "Copy failed" : ""
|
|
78
83
|
}) : null
|
|
79
84
|
]
|
|
80
85
|
});
|
|
81
86
|
});
|
|
82
87
|
//#endregion
|
|
83
|
-
export {
|
|
88
|
+
export { p as CodeBlock };
|
|
84
89
|
|
|
85
90
|
//# sourceMappingURL=CodeBlock.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeBlock.js","names":["styles","CodeBlock_module_default","CodeBlock","forwardRef","rest","copy","setCopy","useState","timer","useRef","captionId","useId","source","code","lines","highlighted","highlightLines","hasHeader","title","language","copyable","onCopy","cx","className","wrap","showLineNumbers","Button","jsx","CheckIcon","CopyIcon","copyLabel","i","line"],"sources":["../../../../src/components/display/CodeBlock/CodeBlock.tsx"],"sourcesContent":["import {\n forwardRef,\n useEffect,\n useId,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from 'react'\nimport { CheckIcon, CopyIcon } from '#icons'\nimport { Button } from '#components/actions/Button'\nimport { cx } from '#utils/cx'\nimport styles from './CodeBlock.module.css'\n\nexport interface CodeBlockProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {\n /** The source, verbatim. Trailing newlines are trimmed. */\n code: string\n /** Shown as a label (\"TypeScript\", \"bash\"). No syntax highlighting — the code is the design. */\n language?: string\n /** Filename or caption shown in the header: `src/queries/keys.ts`. */\n title?: ReactNode\n /** Number the lines in a gutter (not copied with a selection). */\n showLineNumbers?: boolean\n /** 1-based line numbers to mark with the theme's highlight colour. */\n highlightLines?: number[]\n /** Show a copy button. Default `true`. */\n copyable?: boolean\n /** Wrap long lines instead of scrolling sideways. */\n wrap?: boolean\n /** Accessible name of the copy button. Default \"Copy code\". */\n copyLabel?: string\n}\n\ntype CopyState = 'idle' | 'copied' | 'failed'\n\n/**\n * A block of source with an optional filename bar, line numbers, highlighted lines and a copy\n * button.\n *\n * @remarks\n * `CodeBlock` shows code: a snippet in an article, a command, a config example. It's a plain\n * surface with an optional title bar, never fake window chrome, and it has no syntax highlighting:\n * the code is the design. Every code sample in these docs is a `CodeBlock`.\n *\n * @privateRemarks\n * A block of source: a snippet in a blog post, a command in a README, a config example.\n * A plain surface with an optional filename bar — no fake window chrome.\n *\n * <CodeBlock title=\"queries/keys.ts\" language=\"TypeScript\" code={source} highlightLines={[3, 4]} />\n */\nexport const CodeBlock = forwardRef<HTMLElement, CodeBlockProps>(function CodeBlock(\n {\n code,\n language,\n title,\n showLineNumbers = false,\n highlightLines,\n copyable = true,\n wrap = false,\n copyLabel = 'Copy code',\n className,\n ...rest\n },\n ref,\n) {\n const [copy, setCopy] = useState<CopyState>('idle')\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const captionId = useId()\n const source = code.replace(/\\n+$/, '')\n const lines = source.split('\\n')\n const highlighted = new Set(highlightLines)\n const hasHeader = Boolean(title) || Boolean(language) || copyable\n\n useEffect(\n () => () => {\n clearTimeout(timer.current)\n },\n [],\n )\n\n const onCopy = async () => {\n clearTimeout(timer.current)\n try {\n await navigator.clipboard.writeText(source)\n setCopy('copied')\n } catch {\n setCopy('failed')\n }\n timer.current = setTimeout(() => {\n setCopy('idle')\n }, 2000)\n }\n\n return (\n <figure\n ref={ref}\n className={cx(styles.codeBlock, className)}\n data-kiln-component=\"\"\n data-wrap={wrap || undefined}\n data-line-numbers={showLineNumbers || undefined}\n aria-labelledby={title ? captionId : undefined}\n {...rest}\n >\n {hasHeader ? (\n <figcaption className={styles.header}>\n {title ? (\n <span id={captionId} className={styles.title}>\n {title}\n </span>\n ) : null}\n {language ? <span className={styles.language}>{language}</span> : null}\n {copyable ? (\n <Button\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n className={styles.copy}\n leadingIcon={copy === 'copied' ? <CheckIcon /> : <CopyIcon />}\n aria-label={copyLabel}\n onClick={() => {\n void onCopy()\n }}\n data-state={copy}\n >\n {copy === 'copied' ? 'Copied' : copy === 'failed' ? 'Copy failed' : 'Copy'}\n </Button>\n ) : null}\n </figcaption>\n ) : null}\n {/* A scrollable region must be reachable by keyboard. */}\n <pre className={styles.pre} tabIndex={0}>\n <code className={styles.code}>\n {lines.map((line, i) => (\n <span\n key={i}\n className={styles.line}\n data-line={showLineNumbers ? i + 1 : undefined}\n data-highlighted={highlighted.has(i + 1) || undefined}\n >\n {line}\n {i < lines.length - 1 ? '\\n' : null}\n </span>\n ))}\n </code>\n </pre>\n {copyable ? (\n <span className={styles.visuallyHidden} role=\"status\" aria-live=\"polite\">\n {copy === 'copied' ? 'Copied to clipboard' : copy === 'failed' ? 'Copy failed' : ''}\n </span>\n ) : null}\n </figure>\n )\n})\n"],"mappings":";;;;;;;AAkDA,IAAaE,IAAYC,EAAwC,SAC/D,EACE,SACA,aACA,UACA,qBAAkB,IAClB,mBACA,cAAW,IACX,UAAO,IACP,eAAY,aACZ,cACA,GAAGC,KAEL,GACA;CACA,IAAM,CAACC,GAAMC,KAAWC,EAAoB,MAAM,GAC5CC,IAAQC,EAAkD,KAAA,CAAS,GACnEC,IAAYC,EAAM,GAClBC,IAASC,EAAK,QAAQ,QAAQ,EAAE,GAChCC,IAAQF,EAAO,MAAM,IAAI,GACzBG,IAAc,IAAI,IAAIC,CAAc,GACpCC,IAAY,EAAQC,KAAU,EAAQC,KAAaC;CAEzD,cACc;EACV,aAAaZ,EAAM,OAAO;CAC5B,GACA,CAAC,CACH;CAEA,IAAMa,IAAS,YAAY;EACzB,aAAab,EAAM,OAAO;EAC1B,IAAI;GAEF,AADA,MAAM,UAAU,UAAU,UAAUI,CAAM,GAC1CN,EAAQ,QAAQ;EAClB,QAAQ;GACN,EAAQ,QAAQ;EAClB;EACA,EAAM,UAAU,iBAAiB;GAC/B,EAAQ,MAAM;EAChB,GAAG,GAAI;CACT;CAEA,OACE,kBAAC,UAAD;EACO;EACL,WAAWgB,EAAGtB,EAAO,WAAWuB,CAAS;EACzC,uBAAoB;EACpB,aAAWC,KAAQ,KAAA;EACnB,qBAAmBC,KAAmB,KAAA;EACtC,mBAAiBP,IAAQR,IAAY,KAAA;EACrC,GAAIN;EAPN,UAAA;GASGa,IACC,kBAAC,cAAD;IAAY,WAAWjB,EAAO;IAA9B,UAAA;KACGkB,IACC,kBAAC,QAAD;MAAM,IAAIR;MAAW,WAAWV,EAAO;MACpC,UAAAkB;KACG,CAAA,IACJ;KACHC,IAAW,kBAAC,QAAD;MAAM,WAAWnB,EAAO;MAAW,UAAAmB;KAAe,CAAA,IAAI;KACjEC,IACC,kBAACM,GAAD;MACE,SAAQ;MACR,MAAK;MACL,MAAK;MACL,WAAW1B,EAAO;MAClB,aAAiC2B,EAApBtB,MAAS,WAAYuB,IAAgBC,GAAjB,CAAY,CAAe;MAC5D,cAAYC;MACZ,eAAe;OACb,EAAY;MACd;MACA,cAAYzB;MAEX,UAAAA,MAAS,WAAW,WAAWA,MAAS,WAAW,gBAAgB;KAC9D,CAAA,IACN;IACM;GACV,CAAA,IAAA;GAEJ,kBAAC,OAAD;IAAK,WAAWL,EAAO;IAAK,UAAU;IACpC,UAAA,kBAAC,QAAD;KAAM,WAAWA,EAAO;KACrB,UAAAc,EAAM,KAAK,GAAM,MAChB,kBAAC,QAAD;MAEE,WAAWd,EAAO;MAClB,aAAWyB,IAAkBM,IAAI,IAAI,KAAA;MACrC,oBAAkBhB,EAAY,IAAIgB,IAAI,CAAC,KAAK,KAAA;MAJ9C,UAAA,CAMGC,GACAD,IAAIjB,EAAM,SAAS,IAAI,OAAO,IAC3B;KAPC,GAAAiB,CAOD,CACP;IACG,CAAA;GACH,CAAA;GACJX,IACC,kBAAC,QAAD;IAAM,WAAWpB,EAAO;IAAgB,MAAK;IAAS,aAAU;IAC7D,UAAAK,MAAS,WAAW,wBAAwBA,MAAS,WAAW,gBAAgB;GAC7E,CAAA,IACJ;EACE;;AAEZ,CAAC"}
|
|
1
|
+
{"version":3,"file":"CodeBlock.js","names":["styles","CodeBlock_module_default","CodeBlock","forwardRef","rest","copy","setCopy","useState","timer","useRef","captionId","useId","source","trimTrailingNewlines","code","lines","lineTokens","tokens","line","token","i","highlighted","highlightLines","hasHeader","title","language","copyable","onCopy","cx","className","wrap","showLineNumbers","Button","jsx","CheckIcon","CopyIcon","copyLabel","j"],"sources":["../../../../src/components/display/CodeBlock/CodeBlock.tsx"],"sourcesContent":["import {\n forwardRef,\n useEffect,\n useId,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from 'react'\nimport { CheckIcon, CopyIcon } from '#icons'\nimport { Button } from '#components/actions/Button'\nimport { cx } from '#utils/cx'\nimport { trimTrailingNewlines } from '#utils/newlines'\nimport styles from './CodeBlock.module.css'\n\n/**\n * What a piece of highlighted source is. Each kind is coloured from the theme's own tokens, so\n * highlighted code follows the theme and the colour mode.\n */\nexport type CodeTokenType =\n | 'keyword'\n | 'string'\n | 'comment'\n | 'constant'\n | 'function'\n | 'type'\n | 'tag'\n | 'attribute'\n | 'punctuation'\n\n/** A run of source text and its kind. A token with no `type` is plain text. */\nexport interface CodeToken {\n content: string\n type?: CodeTokenType\n}\n\nexport interface CodeBlockProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {\n /** The source, verbatim. Trailing newlines are trimmed. */\n code: string\n /** Shown as a label (\"TypeScript\", \"bash\"). It doesn't turn on highlighting: `tokens` does. */\n language?: string\n /**\n * Syntax highlighting for `code`: one array of tokens per line, as `highlight` from\n * `@mitcsutt/kiln-ui/highlight` returns them. Without it, or when the tokens don't spell out\n * `code` line for line (say, tokens from a previous `code`), the code is plain.\n */\n tokens?: CodeToken[][]\n /** Filename or caption shown in the header: `src/queries/keys.ts`. */\n title?: ReactNode\n /** Number the lines in a gutter (not copied with a selection). */\n showLineNumbers?: boolean\n /** 1-based line numbers to mark with the theme's highlight colour. */\n highlightLines?: number[]\n /** Show a copy button. Default `true`. */\n copyable?: boolean\n /** Wrap long lines instead of scrolling sideways. */\n wrap?: boolean\n /** Accessible name of the copy button. Default \"Copy code\". */\n copyLabel?: string\n}\n\ntype CopyState = 'idle' | 'copied' | 'failed'\n\n/**\n * A block of source with an optional filename bar, line numbers, highlighted lines and a copy\n * button.\n *\n * @remarks\n * `CodeBlock` shows code: a snippet in an article, a command, a config example. It's a plain\n * surface with an optional title bar, never fake window chrome. It's plain unless you pass\n * `tokens`: `highlight` from `@mitcsutt/kiln-ui/highlight` turns JavaScript, JSX, TypeScript and\n * TSX into tokens, coloured from the theme. Every code sample in these docs is a `CodeBlock`.\n *\n * ## Highlighting\n *\n * `highlight(code, language)` resolves to the tokens for `code`, or to `undefined` for a language\n * other than `js`, `jsx`, `ts` or `tsx` (or `javascript` and `typescript`), which `CodeBlock`\n * shows as plain code. It runs [Shiki](https://shiki.style), an optional peer dependency, so\n * install `shiki` to use it. The main `@mitcsutt/kiln-ui` entry never loads it.\n *\n * Keywords and tags take the accent, strings, constants, functions and types take the theme's\n * tones, and comments and punctuation are muted. A custom theme needs no code palette of its own.\n *\n * The tokens are plain data, so highlight wherever the code is ready. In a server component or\n * at build time, `await` it and pass the tokens to the client component that renders the\n * `CodeBlock`, and the browser downloads no highlighter. In the browser, call it in an effect, as\n * in the example above: Shiki and the grammar load on the first call.\n *\n * ```tsx\n * import { highlight } from '@mitcsutt/kiln-ui/highlight'\n *\n * export async function Snippet({ source }: { source: string }) {\n * const tokens = await highlight(source, 'tsx')\n * return <SnippetBlock code={source} tokens={tokens} />\n * }\n * ```\n *\n * @privateRemarks\n * A block of source: a snippet in a blog post, a command in a README, a config example.\n * A plain surface with an optional filename bar — no fake window chrome.\n *\n * <CodeBlock title=\"queries/keys.ts\" language=\"TypeScript\" code={source} highlightLines={[3, 4]} />\n */\nexport const CodeBlock = forwardRef<HTMLElement, CodeBlockProps>(function CodeBlock(\n {\n code,\n language,\n tokens,\n title,\n showLineNumbers = false,\n highlightLines,\n copyable = true,\n wrap = false,\n copyLabel = 'Copy code',\n className,\n ...rest\n },\n ref,\n) {\n const [copy, setCopy] = useState<CopyState>('idle')\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const captionId = useId()\n const source = trimTrailingNewlines(code)\n const lines = source.split('\\n')\n const lineTokens =\n tokens?.length === lines.length &&\n tokens.every((line, i) => line.map((token) => token.content).join('') === lines[i])\n ? tokens\n : undefined\n const highlighted = new Set(highlightLines)\n const hasHeader = Boolean(title) || Boolean(language) || copyable\n\n useEffect(\n () => () => {\n clearTimeout(timer.current)\n },\n [],\n )\n\n const onCopy = async () => {\n clearTimeout(timer.current)\n try {\n await navigator.clipboard.writeText(source)\n setCopy('copied')\n } catch {\n setCopy('failed')\n }\n timer.current = setTimeout(() => {\n setCopy('idle')\n }, 2000)\n }\n\n return (\n <figure\n ref={ref}\n className={cx(styles.codeBlock, className)}\n data-kiln-component=\"\"\n data-wrap={wrap || undefined}\n data-line-numbers={showLineNumbers || undefined}\n aria-labelledby={title ? captionId : undefined}\n {...rest}\n >\n {hasHeader ? (\n <figcaption className={styles.header}>\n {title ? (\n <span id={captionId} className={styles.title}>\n {title}\n </span>\n ) : null}\n {language ? <span className={styles.language}>{language}</span> : null}\n {copyable ? (\n <Button\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n className={styles.copy}\n leadingIcon={copy === 'copied' ? <CheckIcon /> : <CopyIcon />}\n aria-label={copyLabel}\n onClick={() => {\n void onCopy()\n }}\n data-state={copy}\n >\n {copy === 'copied' ? 'Copied' : copy === 'failed' ? 'Copy failed' : 'Copy'}\n </Button>\n ) : null}\n </figcaption>\n ) : null}\n {/* A scrollable region must be reachable by keyboard. */}\n <pre className={styles.pre} tabIndex={0}>\n <code className={styles.code}>\n {lines.map((line, i) => (\n <span\n key={i}\n className={styles.line}\n data-line={showLineNumbers ? i + 1 : undefined}\n data-highlighted={highlighted.has(i + 1) || undefined}\n >\n {lineTokens?.[i]\n ? lineTokens[i].map((token, j) =>\n token.type ? (\n <span key={j} className={styles.token} data-token={token.type}>\n {token.content}\n </span>\n ) : (\n token.content\n ),\n )\n : line}\n {i < lines.length - 1 ? '\\n' : null}\n </span>\n ))}\n </code>\n </pre>\n {copyable ? (\n <span className={styles.visuallyHidden} role=\"status\" aria-live=\"polite\">\n {copy === 'copied' ? 'Copied to clipboard' : copy === 'failed' ? 'Copy failed' : ''}\n </span>\n ) : null}\n </figure>\n )\n})\n"],"mappings":";;;;;;;;AAuGA,IAAaE,IAAYC,EAAwC,SAC/D,EACE,SACA,aACA,WACA,UACA,qBAAkB,IAClB,mBACA,cAAW,IACX,UAAO,IACP,eAAY,aACZ,cACA,GAAGC,KAEL,GACA;CACA,IAAM,CAACC,GAAMC,KAAWC,EAAoB,MAAM,GAC5CC,IAAQC,EAAkD,KAAA,CAAS,GACnEC,IAAYC,EAAM,GAClBC,IAASC,EAAqBC,CAAI,GAClCC,IAAQH,EAAO,MAAM,IAAI,GACzBI,IACJC,GAAQ,WAAWF,EAAM,UACzBE,EAAO,OAAO,GAAM,MAAMC,EAAK,KAAK,MAAUC,EAAM,OAAO,CAAC,CAAC,KAAK,EAAE,MAAMJ,EAAMK,EAAE,IAC9EH,IACA,KAAA,GACAI,IAAc,IAAI,IAAIC,CAAc,GACpCC,IAAY,EAAQC,KAAU,EAAQC,KAAaC;CAEzD,cACc;EACV,aAAalB,EAAM,OAAO;CAC5B,GACA,CAAC,CACH;CAEA,IAAMmB,IAAS,YAAY;EACzB,aAAanB,EAAM,OAAO;EAC1B,IAAI;GAEF,AADA,MAAM,UAAU,UAAU,UAAUI,CAAM,GAC1CN,EAAQ,QAAQ;EAClB,QAAQ;GACN,EAAQ,QAAQ;EAClB;EACA,EAAM,UAAU,iBAAiB;GAC/B,EAAQ,MAAM;EAChB,GAAG,GAAI;CACT;CAEA,OACE,kBAAC,UAAD;EACO;EACL,WAAWsB,EAAG5B,EAAO,WAAW6B,CAAS;EACzC,uBAAoB;EACpB,aAAWC,KAAQ,KAAA;EACnB,qBAAmBC,KAAmB,KAAA;EACtC,mBAAiBP,IAAQd,IAAY,KAAA;EACrC,GAAIN;EAPN,UAAA;GASGmB,IACC,kBAAC,cAAD;IAAY,WAAWvB,EAAO;IAA9B,UAAA;KACGwB,IACC,kBAAC,QAAD;MAAM,IAAId;MAAW,WAAWV,EAAO;MACpC,UAAAwB;KACG,CAAA,IACJ;KACHC,IAAW,kBAAC,QAAD;MAAM,WAAWzB,EAAO;MAAW,UAAAyB;KAAe,CAAA,IAAI;KACjEC,IACC,kBAACM,GAAD;MACE,SAAQ;MACR,MAAK;MACL,MAAK;MACL,WAAWhC,EAAO;MAClB,aAAiCiC,EAApB5B,MAAS,WAAY6B,IAAgBC,GAAjB,CAAY,CAAe;MAC5D,cAAYC;MACZ,eAAe;OACb,EAAY;MACd;MACA,cAAY/B;MAEX,UAAAA,MAAS,WAAW,WAAWA,MAAS,WAAW,gBAAgB;KAC9D,CAAA,IACN;IACM;GACV,CAAA,IAAA;GAEJ,kBAAC,OAAD;IAAK,WAAWL,EAAO;IAAK,UAAU;IACpC,UAAA,kBAAC,QAAD;KAAM,WAAWA,EAAO;KACrB,UAAAe,EAAM,KAAK,GAAM,MAChB,kBAAC,QAAD;MAEE,WAAWf,EAAO;MAClB,aAAW+B,IAAkBX,IAAI,IAAI,KAAA;MACrC,oBAAkBC,EAAY,IAAID,IAAI,CAAC,KAAK,KAAA;MAJ9C,UAAA,CAMGJ,IAAaI,KACVJ,EAAWI,EAAE,CAAC,KAAK,GAAO,MACxBD,EAAM,OACJ,kBAAC,QAAD;OAAc,WAAWnB,EAAO;OAAO,cAAYmB,EAAM;OACtD,UAAAA,EAAM;MACH,GAFKkB,CAEL,IAENlB,EAAM,OAEV,IACAD,GACHE,IAAIL,EAAM,SAAS,IAAI,OAAO,IAC3B;KAjBC,GAAAK,CAiBD,CACP;IACG,CAAA;GACH,CAAA;GACJM,IACC,kBAAC,QAAD;IAAM,WAAW1B,EAAO;IAAgB,MAAK;IAAS,aAAU;IAC7D,UAAAK,MAAS,WAAW,wBAAwBA,MAAS,WAAW,gBAAgB;GAC7E,CAAA,IACJ;EACE;;AAEZ,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/components/display/CodeBlock/CodeBlock.module.css
|
|
2
|
-
var e = "kiln-CodeBlock__codeBlock", t = "kiln-CodeBlock__header", n = "kiln-CodeBlock__title", r = "kiln-CodeBlock__language", i = "kiln-CodeBlock__copy", a = "kiln-CodeBlock__pre", o = "kiln-CodeBlock__code", s = "kiln-CodeBlock__line", c = "kiln-
|
|
2
|
+
var e = "kiln-CodeBlock__codeBlock", t = "kiln-CodeBlock__header", n = "kiln-CodeBlock__title", r = "kiln-CodeBlock__language", i = "kiln-CodeBlock__copy", a = "kiln-CodeBlock__pre", o = "kiln-CodeBlock__code", s = "kiln-CodeBlock__line", c = "kiln-CodeBlock__token", l = "kiln-CodeBlock__visuallyHidden", u = {
|
|
3
3
|
codeBlock: e,
|
|
4
4
|
header: t,
|
|
5
5
|
title: n,
|
|
@@ -8,9 +8,10 @@ var e = "kiln-CodeBlock__codeBlock", t = "kiln-CodeBlock__header", n = "kiln-Cod
|
|
|
8
8
|
pre: a,
|
|
9
9
|
code: o,
|
|
10
10
|
line: s,
|
|
11
|
-
|
|
11
|
+
token: c,
|
|
12
|
+
visuallyHidden: l
|
|
12
13
|
};
|
|
13
14
|
//#endregion
|
|
14
|
-
export { o as code, e as codeBlock, i as copy,
|
|
15
|
+
export { o as code, e as codeBlock, i as copy, u as default, t as header, r as language, s as line, a as pre, n as title, c as token, l as visuallyHidden };
|
|
15
16
|
|
|
16
17
|
//# sourceMappingURL=CodeBlock.module.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeBlock.module.js","names":["codeBlock","header","title","language","copy","pre","code","line","visuallyHidden","CodeBlock_module_default"],"sources":["../../../../src/components/display/CodeBlock/CodeBlock.module.css"],"sourcesContent":["/*\n * Component tokens (themes may set any of these on [data-theme]):\n * --codeblock-bg default var(--color-surface-sunken)\n * --codeblock-radius default var(--radius-surface)\n * --codeblock-size default var(--text-xs)\n * --codeblock-highlight highlighted line fill, default var(--color-highlight)\n *\n * One edge: a hairline. The header is separated by the same rule, not a fill.\n */\n/* Zero-specificity reset, so a surrounding flow (Prose, Stack) or a className can space it. */\n:where(.codeBlock) {\n margin: 0;\n}\n.codeBlock {\n --_pad: var(--space-4);\n\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n overflow: hidden;\n border: var(--border-width) solid var(--color-line);\n border-radius: var(--codeblock-radius, var(--radius-surface));\n background: var(--codeblock-bg, var(--color-surface-sunken));\n color: var(--color-ink);\n}\n\n.header {\n display: flex;\n align-items: center;\n gap: var(--space-3);\n min-block-size: calc(var(--control-sm) + var(--space-2));\n padding-inline: var(--_pad) var(--space-2);\n border-block-end: var(--border-width) solid var(--color-line);\n font-family: var(--font-text);\n font-size: var(--text-xs);\n line-height: var(--leading-snug);\n}\n\n.title {\n min-inline-size: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-ink);\n font-weight: var(--label-weight);\n}\n\n.language {\n color: var(--color-ink-subtle);\n}\n.title + .language::before {\n content: '·';\n margin-inline-end: var(--space-3);\n}\n\n.copy {\n margin-inline-start: auto;\n color: var(--color-ink-muted);\n}\n.copy[data-state='copied'] {\n color: var(--tone-positive-text);\n}\n.copy[data-state='failed'] {\n color: var(--tone-critical-text);\n}\n\n.pre {\n margin: 0;\n padding-block: var(--space-3);\n overflow-x: auto;\n font-family: var(--font-mono);\n font-stretch: var(--mono-stretch);\n font-size: var(--codeblock-size, var(--text-xs));\n font-variant-ligatures: none;\n line-height: 1.7;\n tab-size: 2;\n white-space: pre;\n scrollbar-width: thin;\n}\n.pre:focus-visible {\n outline: 2px solid var(--color-focus);\n outline-offset: -2px;\n}\n\n.code {\n display: inline-block;\n min-inline-size: 100%;\n font: inherit;\n}\n\n.line {\n display: block;\n position: relative;\n padding-inline: var(--_pad);\n}\n\n.codeBlock[data-line-numbers] .line {\n padding-inline-start: calc(var(--_pad) + 4ch);\n}\n.codeBlock[data-line-numbers] .line::before {\n content: attr(data-line);\n position: absolute;\n inset-inline-start: 0;\n inline-size: calc(var(--_pad) + 2.5ch);\n padding-inline-end: 1ch;\n text-align: end;\n color: var(--color-ink-subtle);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n\n.line[data-highlighted] {\n background: var(--codeblock-highlight, var(--color-highlight));\n color: var(--color-highlight-ink);\n}\n.line[data-highlighted]::before {\n color: inherit;\n}\n\n.codeBlock[data-wrap] .pre {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.codeBlock[data-wrap] .code {\n display: block;\n}\n\n.visuallyHidden {\n position: absolute;\n inline-size: 1px;\n block-size: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"CodeBlock.module.js","names":["codeBlock","header","title","language","copy","pre","code","line","token","visuallyHidden","CodeBlock_module_default"],"sources":["../../../../src/components/display/CodeBlock/CodeBlock.module.css"],"sourcesContent":["/*\n * Component tokens (themes may set any of these on [data-theme]):\n * --codeblock-bg default var(--color-surface-sunken)\n * --codeblock-radius default var(--radius-surface)\n * --codeblock-size default var(--text-xs)\n * --codeblock-highlight highlighted line fill, default var(--color-highlight)\n *\n * One edge: a hairline. The header is separated by the same rule, not a fill.\n */\n/* Zero-specificity reset, so a surrounding flow (Prose, Stack) or a className can space it. */\n:where(.codeBlock) {\n margin: 0;\n}\n.codeBlock {\n --_pad: var(--space-4);\n\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n overflow: hidden;\n border: var(--border-width) solid var(--color-line);\n border-radius: var(--codeblock-radius, var(--radius-surface));\n background: var(--codeblock-bg, var(--color-surface-sunken));\n color: var(--color-ink);\n}\n\n.header {\n display: flex;\n align-items: center;\n gap: var(--space-3);\n min-block-size: calc(var(--control-sm) + var(--space-2));\n padding-inline: var(--_pad) var(--space-2);\n border-block-end: var(--border-width) solid var(--color-line);\n font-family: var(--font-text);\n font-size: var(--text-xs);\n line-height: var(--leading-snug);\n}\n\n.title {\n min-inline-size: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-ink);\n font-weight: var(--label-weight);\n}\n\n.language {\n color: var(--color-ink-subtle);\n}\n.title + .language::before {\n content: '·';\n margin-inline-end: var(--space-3);\n}\n\n.copy {\n margin-inline-start: auto;\n color: var(--color-ink-muted);\n}\n.copy[data-state='copied'] {\n color: var(--tone-positive-text);\n}\n.copy[data-state='failed'] {\n color: var(--tone-critical-text);\n}\n\n.pre {\n margin: 0;\n padding-block: var(--space-3);\n overflow-x: auto;\n font-family: var(--font-mono);\n font-stretch: var(--mono-stretch);\n font-size: var(--codeblock-size, var(--text-xs));\n font-variant-ligatures: none;\n line-height: 1.7;\n tab-size: 2;\n white-space: pre;\n scrollbar-width: thin;\n}\n.pre:focus-visible {\n outline: 2px solid var(--color-focus);\n outline-offset: -2px;\n}\n\n.code {\n display: inline-block;\n min-inline-size: 100%;\n font: inherit;\n}\n\n.line {\n display: block;\n position: relative;\n padding-inline: var(--_pad);\n}\n\n.codeBlock[data-line-numbers] .line {\n padding-inline-start: calc(var(--_pad) + 4ch);\n}\n.codeBlock[data-line-numbers] .line::before {\n content: attr(data-line);\n position: absolute;\n inset-inline-start: 0;\n inline-size: calc(var(--_pad) + 2.5ch);\n padding-inline-end: 1ch;\n text-align: end;\n color: var(--color-ink-subtle);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n\n/*\n * Highlighted tokens (the `tokens` prop) take the theme's own text colours, so a theme needs no\n * code palette of its own: keywords in the accent, strings, constants and names in the tones.\n */\n.token[data-token='keyword'],\n.token[data-token='tag'] {\n color: var(--color-accent-text);\n}\n.token[data-token='string'] {\n color: var(--tone-positive-text);\n}\n.token[data-token='constant'] {\n color: var(--tone-caution-text);\n}\n.token[data-token='function'],\n.token[data-token='type'] {\n color: var(--tone-info-text);\n}\n.token[data-token='attribute'],\n.token[data-token='punctuation'] {\n color: var(--color-ink-muted);\n}\n.token[data-token='comment'] {\n color: var(--color-ink-subtle);\n}\n\n.line[data-highlighted] {\n background: var(--codeblock-highlight, var(--color-highlight));\n color: var(--color-highlight-ink);\n}\n.line[data-highlighted]::before,\n.line[data-highlighted] .token {\n color: inherit;\n}\n\n.codeBlock[data-wrap] .pre {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.codeBlock[data-wrap] .code {\n display: block;\n}\n\n.visuallyHidden {\n position: absolute;\n inline-size: 1px;\n block-size: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n"],"mappings":""}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { CodeBlock } from './CodeBlock.js';
|
|
2
|
-
export type { CodeBlockProps } from './CodeBlock.js';
|
|
2
|
+
export type { CodeBlockProps, CodeToken, CodeTokenType } from './CodeBlock.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CodeToken } from '../components/display/CodeBlock/index.js';
|
|
2
|
+
/** The languages `highlight` understands, with their usual aliases. */
|
|
3
|
+
export type HighlightLanguage = 'js' | 'javascript' | 'jsx' | 'ts' | 'typescript' | 'tsx';
|
|
4
|
+
/**
|
|
5
|
+
* Tokens for `CodeBlock`'s `tokens` prop: one array per line of `code`, each token tagged with
|
|
6
|
+
* what it is (keyword, string, comment…) so the theme colours it. Resolves to `undefined` for a
|
|
7
|
+
* language it doesn't know, which `CodeBlock` shows as plain code.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Install `shiki` alongside kiln-ui to use it. Call it wherever your code is ready: in a server
|
|
11
|
+
* component or at build time, so the browser downloads no highlighter, or in the browser, where
|
|
12
|
+
* Shiki and the grammar load on the first call.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const tokens = await highlight(source, 'tsx')
|
|
16
|
+
* return <CodeBlock code={source} language="TSX" tokens={tokens} />
|
|
17
|
+
*/
|
|
18
|
+
export declare function highlight(code: string, language: string): Promise<CodeToken[][] | undefined>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { trimTrailingNewlines as e } from "../utils/newlines.js";
|
|
2
|
+
//#region src/highlight/index.ts
|
|
3
|
+
var t = {
|
|
4
|
+
js: "javascript",
|
|
5
|
+
javascript: "javascript",
|
|
6
|
+
jsx: "jsx",
|
|
7
|
+
ts: "typescript",
|
|
8
|
+
typescript: "typescript",
|
|
9
|
+
tsx: "tsx"
|
|
10
|
+
}, n = {
|
|
11
|
+
javascript: () => import("shiki/langs/javascript.mjs"),
|
|
12
|
+
jsx: () => import("shiki/langs/jsx.mjs"),
|
|
13
|
+
typescript: () => import("shiki/langs/typescript.mjs"),
|
|
14
|
+
tsx: () => import("shiki/langs/tsx.mjs")
|
|
15
|
+
}, r = [
|
|
16
|
+
"keyword",
|
|
17
|
+
"string",
|
|
18
|
+
"comment",
|
|
19
|
+
"constant",
|
|
20
|
+
"function",
|
|
21
|
+
"type",
|
|
22
|
+
"tag",
|
|
23
|
+
"attribute",
|
|
24
|
+
"punctuation"
|
|
25
|
+
], i = (e) => `#0000${(r.indexOf(e) + 1).toString(16).padStart(2, "0")}`, a = new Map(r.map((e) => [i(e), e])), o = "#000000", s = {
|
|
26
|
+
name: "kiln",
|
|
27
|
+
type: "light",
|
|
28
|
+
fg: o,
|
|
29
|
+
bg: "#ffffff",
|
|
30
|
+
settings: [
|
|
31
|
+
[["comment", "punctuation.definition.comment"], "comment"],
|
|
32
|
+
[["string", "punctuation.definition.string"], "string"],
|
|
33
|
+
[["meta.template.expression"], null],
|
|
34
|
+
[[
|
|
35
|
+
"constant.numeric",
|
|
36
|
+
"constant.language",
|
|
37
|
+
"constant.character",
|
|
38
|
+
"variable.language"
|
|
39
|
+
], "constant"],
|
|
40
|
+
[[
|
|
41
|
+
"keyword",
|
|
42
|
+
"storage",
|
|
43
|
+
"keyword.operator.new",
|
|
44
|
+
"keyword.operator.expression"
|
|
45
|
+
], "keyword"],
|
|
46
|
+
[["entity.name.function", "support.function"], "function"],
|
|
47
|
+
[[
|
|
48
|
+
"entity.name.type",
|
|
49
|
+
"entity.name.class",
|
|
50
|
+
"entity.other.inherited-class",
|
|
51
|
+
"support.type",
|
|
52
|
+
"support.class"
|
|
53
|
+
], "type"],
|
|
54
|
+
[["entity.name.tag"], "tag"],
|
|
55
|
+
[["entity.other.attribute-name"], "attribute"],
|
|
56
|
+
[[
|
|
57
|
+
"punctuation",
|
|
58
|
+
"meta.brace",
|
|
59
|
+
"keyword.operator"
|
|
60
|
+
], "punctuation"],
|
|
61
|
+
[["punctuation.definition.string.template.begin", "punctuation.definition.string.template.end"], "string"],
|
|
62
|
+
[["punctuation.definition.template-expression"], "punctuation"]
|
|
63
|
+
].map(([e, t]) => ({
|
|
64
|
+
scope: e,
|
|
65
|
+
settings: { foreground: t ? i(t) : o }
|
|
66
|
+
}))
|
|
67
|
+
}, c, l = /* @__PURE__ */ new Map();
|
|
68
|
+
function u(e) {
|
|
69
|
+
return c ??= Promise.all([import("shiki/core"), import("shiki/engine/javascript")]).then(([{ createHighlighterCore: e }, { createJavaScriptRegexEngine: t }]) => e({
|
|
70
|
+
themes: [s],
|
|
71
|
+
langs: [],
|
|
72
|
+
engine: t()
|
|
73
|
+
})), c.then(async (t) => {
|
|
74
|
+
let r = l.get(e);
|
|
75
|
+
return r || (r = n[e]().then((e) => t.loadLanguage(e.default)), l.set(e, r)), await r, t;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function d(e) {
|
|
79
|
+
return Object.hasOwn(t, e);
|
|
80
|
+
}
|
|
81
|
+
async function f(n, r) {
|
|
82
|
+
let i = r.toLowerCase();
|
|
83
|
+
if (!d(i)) return;
|
|
84
|
+
let o = t[i];
|
|
85
|
+
return (await u(o)).codeToTokensBase(e(n), {
|
|
86
|
+
lang: o,
|
|
87
|
+
theme: "kiln"
|
|
88
|
+
}).map((e) => {
|
|
89
|
+
let t = [];
|
|
90
|
+
for (let { content: n, color: r } of e) {
|
|
91
|
+
let e = r ? a.get(r.toLowerCase()) : void 0, i = t.at(-1);
|
|
92
|
+
i && i.type === e ? i.content += n : t.push(e ? {
|
|
93
|
+
content: n,
|
|
94
|
+
type: e
|
|
95
|
+
} : { content: n });
|
|
96
|
+
}
|
|
97
|
+
return t;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { f as highlight };
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["GRAMMARS","LOADERS","TYPES","colourOf","type","typeOf","PLAIN","THEME","scope","highlighter","loaded","load","createHighlighterCore","createJavaScriptRegexEngine","language","grammar","core","module","isHighlightLanguage","highlight","id","trimTrailingNewlines","code","tokens","line","color","previous","content"],"sources":["../../src/highlight/index.ts"],"sourcesContent":["/*\n * @mitcsutt/kiln-ui/highlight: syntax highlighting for CodeBlock, kept out of the main entry.\n * It runs Shiki (an optional peer dependency) with its JavaScript regex engine, and loads Shiki\n * and each grammar only when the first block in that language is highlighted.\n */\nimport type { CodeToken, CodeTokenType } from '#components/display/CodeBlock'\nimport { trimTrailingNewlines } from '#utils/newlines'\nimport type { HighlighterCore, LanguageRegistration, ThemeRegistration } from 'shiki/core'\n\n/** The languages `highlight` understands, with their usual aliases. */\nexport type HighlightLanguage = 'js' | 'javascript' | 'jsx' | 'ts' | 'typescript' | 'tsx'\n\ntype Grammar = 'javascript' | 'jsx' | 'typescript' | 'tsx'\n\nconst GRAMMARS: Record<HighlightLanguage, Grammar> = {\n js: 'javascript',\n javascript: 'javascript',\n jsx: 'jsx',\n ts: 'typescript',\n typescript: 'typescript',\n tsx: 'tsx',\n}\n\nconst LOADERS: Record<Grammar, () => Promise<{ default: LanguageRegistration[] }>> = {\n javascript: () => import('shiki/langs/javascript.mjs'),\n jsx: () => import('shiki/langs/jsx.mjs'),\n typescript: () => import('shiki/langs/typescript.mjs'),\n tsx: () => import('shiki/langs/tsx.mjs'),\n}\n\nconst TYPES: CodeTokenType[] = [\n 'keyword',\n 'string',\n 'comment',\n 'constant',\n 'function',\n 'type',\n 'tag',\n 'attribute',\n 'punctuation',\n]\n\n/** Each token type gets a stand-in colour, which `highlight` reads back as the type. */\nconst colourOf = (type: CodeTokenType) =>\n `#0000${(TYPES.indexOf(type) + 1).toString(16).padStart(2, '0')}`\nconst typeOf = new Map(TYPES.map((type) => [colourOf(type), type]))\nconst PLAIN = '#000000'\n\n/** TextMate scopes → token types. A deeper or longer scope wins, so the exceptions follow. */\nconst RULES: [string[], CodeTokenType | null][] = [\n [['comment', 'punctuation.definition.comment'], 'comment'],\n [['string', 'punctuation.definition.string'], 'string'],\n [['meta.template.expression'], null],\n [\n ['constant.numeric', 'constant.language', 'constant.character', 'variable.language'],\n 'constant',\n ],\n [['keyword', 'storage', 'keyword.operator.new', 'keyword.operator.expression'], 'keyword'],\n [['entity.name.function', 'support.function'], 'function'],\n [\n [\n 'entity.name.type',\n 'entity.name.class',\n 'entity.other.inherited-class',\n 'support.type',\n 'support.class',\n ],\n 'type',\n ],\n [['entity.name.tag'], 'tag'],\n [['entity.other.attribute-name'], 'attribute'],\n [['punctuation', 'meta.brace', 'keyword.operator'], 'punctuation'],\n [\n ['punctuation.definition.string.template.begin', 'punctuation.definition.string.template.end'],\n 'string',\n ],\n [['punctuation.definition.template-expression'], 'punctuation'],\n]\n\nconst THEME: ThemeRegistration = {\n name: 'kiln',\n type: 'light',\n fg: PLAIN,\n bg: '#ffffff',\n settings: RULES.map(([scope, type]) => ({\n scope,\n settings: { foreground: type ? colourOf(type) : PLAIN },\n })),\n}\n\nlet highlighter: Promise<HighlighterCore> | undefined\nconst loaded = new Map<Grammar, Promise<void>>()\n\nfunction load(grammar: Grammar): Promise<HighlighterCore> {\n highlighter ??= Promise.all([import('shiki/core'), import('shiki/engine/javascript')]).then(\n ([{ createHighlighterCore }, { createJavaScriptRegexEngine }]) =>\n createHighlighterCore({ themes: [THEME], langs: [], engine: createJavaScriptRegexEngine() }),\n )\n return highlighter.then(async (core) => {\n let language = loaded.get(grammar)\n if (!language) {\n language = LOADERS[grammar]().then((module) => core.loadLanguage(module.default))\n loaded.set(grammar, language)\n }\n await language\n return core\n })\n}\n\nfunction isHighlightLanguage(language: string): language is HighlightLanguage {\n return Object.hasOwn(GRAMMARS, language)\n}\n\n/**\n * Tokens for `CodeBlock`'s `tokens` prop: one array per line of `code`, each token tagged with\n * what it is (keyword, string, comment…) so the theme colours it. Resolves to `undefined` for a\n * language it doesn't know, which `CodeBlock` shows as plain code.\n *\n * @remarks\n * Install `shiki` alongside kiln-ui to use it. Call it wherever your code is ready: in a server\n * component or at build time, so the browser downloads no highlighter, or in the browser, where\n * Shiki and the grammar load on the first call.\n *\n * @example\n * const tokens = await highlight(source, 'tsx')\n * return <CodeBlock code={source} language=\"TSX\" tokens={tokens} />\n */\nexport async function highlight(\n code: string,\n language: string,\n): Promise<CodeToken[][] | undefined> {\n const id = language.toLowerCase()\n if (!isHighlightLanguage(id)) return undefined\n const grammar = GRAMMARS[id]\n const core = await load(grammar)\n const lines = core.codeToTokensBase(trimTrailingNewlines(code), { lang: grammar, theme: 'kiln' })\n return lines.map((line) => {\n const tokens: CodeToken[] = []\n for (const { content, color } of line) {\n const type = color ? typeOf.get(color.toLowerCase()) : undefined\n const previous = tokens.at(-1)\n if (previous && previous.type === type) previous.content += content\n else tokens.push(type ? { content, type } : { content })\n }\n return tokens\n })\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -175,7 +175,7 @@ export type { MediaProps, MediaRatio, MediaFit, MediaRadius, MediaSize, } from '
|
|
|
175
175
|
export { Marquee } from './components/display/Marquee/index.js';
|
|
176
176
|
export type { MarqueeProps, MarqueeSpeed, MarqueeDirection, MarqueeSurface, } from './components/display/Marquee/index.js';
|
|
177
177
|
export { CodeBlock } from './components/display/CodeBlock/index.js';
|
|
178
|
-
export type { CodeBlockProps } from './components/display/CodeBlock/index.js';
|
|
178
|
+
export type { CodeBlockProps, CodeToken, CodeTokenType } from './components/display/CodeBlock/index.js';
|
|
179
179
|
export { Stamp } from './components/display/Stamp/index.js';
|
|
180
180
|
export type { StampProps, StampPlacement } from './components/display/Stamp/index.js';
|
|
181
181
|
export { Progress } from './components/display/Progress/index.js';
|