@vortexlabs/kirari-rehype 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ugi (https://ugi.dev)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,174 @@
1
- # Temporary Holding Version
1
+ # @vortexlabs/kirari-rehype
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Highlight code blocks in rendered Markdown and MDX with Kirari. This optional
4
+ package converts Kirari tokens directly into structured HTML syntax tree nodes.
5
+ It does not generate raw HTML or require a DOM.
6
+
7
+ Run it while building your content to send highlighted HTML and CSS to the
8
+ browser, with no client-side highlighter. The integration is separate from the
9
+ core package and does not change its bundle.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @vortexlabs/kirari @vortexlabs/kirari-rehype
15
+ ```
16
+
17
+ The plugin uses your application's Kirari through a peer dependency.
18
+ Inside this workspace, use `workspace:*` for both packages instead.
19
+
20
+ ## Markdown to HTML
21
+
22
+ Install `unified`, `remark-parse`, `remark-rehype`, and `rehype-stringify` in your
23
+ application if it does not already have a Markdown pipeline.
24
+
25
+ ```ts
26
+ import { unified } from 'unified'
27
+ import remarkParse from 'remark-parse'
28
+ import remarkRehype from 'remark-rehype'
29
+ import rehypeStringify from 'rehype-stringify'
30
+ import rehypeKirari from '@vortexlabs/kirari-rehype'
31
+ import { javascript } from '@vortexlabs/kirari/languages/javascript'
32
+ import { typescript } from '@vortexlabs/kirari/languages/typescript'
33
+
34
+ const processor = unified()
35
+ .use(remarkParse)
36
+ .use(remarkRehype)
37
+ .use(rehypeKirari, { languages: [javascript, typescript] })
38
+ .use(rehypeStringify)
39
+
40
+ const source = '```js\nconst answer = 42\n```'
41
+ const html = String(await processor.process(source))
42
+ ```
43
+
44
+ Import the optional theme once from your application's stylesheet entry point:
45
+
46
+ ```ts
47
+ import '@vortexlabs/kirari/theme.css'
48
+ ```
49
+
50
+ The plugin adds `.kirari` to the enclosing `<pre>`, preserves existing properties
51
+ and the code element's `language-*` class, and adds `.kirari-*` syntax spans.
52
+ Customize colors with variables such as `--kirari-keyword` and `--kirari-variable`.
53
+
54
+ ## MDX
55
+
56
+ Pass the same plugin to the MDX compiler's `rehypePlugins` option:
57
+
58
+ ```ts
59
+ import { compile } from '@mdx-js/mdx'
60
+ import rehypeKirari from '@vortexlabs/kirari-rehype'
61
+ import { javascript } from '@vortexlabs/kirari/languages/javascript'
62
+
63
+ const markdown = '```js\nconst answer = 42\n```'
64
+ const compiled = await compile(markdown, {
65
+ rehypePlugins: [[rehypeKirari, { languages: [javascript] }]],
66
+ })
67
+ ```
68
+
69
+ Frameworks that configure MDX expose this option in different places. Keep the
70
+ theme in the application entry point; it does not belong in compiler options.
71
+ Custom MDX `pre` and `code` components should forward the generated properties
72
+ and children.
73
+
74
+ ## Register only what you need
75
+
76
+ No languages are registered by default. Import each definition and pass it in
77
+ `languages`. Labels resolve through [Kirari's language registry](../../docs/language-integration.md),
78
+ so `js` selects registered JavaScript and `ts` selects registered TypeScript.
79
+ There is no language detection or automatic grammar loading. JSX and TSX need
80
+ their own definitions.
81
+
82
+ The plugin only highlights labelled, registered `<code>` elements directly
83
+ inside `<pre>` whose children are all text nodes. It leaves unlabelled code,
84
+ unknown languages, inline code, and code with existing element children
85
+ untouched. A `no-highlight` or `nohighlight` class on either `pre` or `code` also
86
+ skips the block. Ordinary Markdown serialization still escapes their text.
87
+
88
+ `@vortexlabs/kirari/markdown` serves a different purpose: it colors Markdown source itself,
89
+ including the fence markers. Use `@vortexlabs/kirari-rehype` when rendering a Markdown
90
+ document into HTML or MDX.
91
+
92
+ ## Lines and fence metadata
93
+
94
+ Add a standalone brace group to a fence's metadata to highlight lines:
95
+
96
+ ````md
97
+ ```js {2,4-6}
98
+ const count = 1
99
+ const doubled = count * 2
100
+
101
+ console.log(count)
102
+ console.log(doubled)
103
+ console.log('done')
104
+ ```
105
+ ````
106
+
107
+ Line numbers are one-based. Valid metadata enables `.kirari-line` wrappers with
108
+ `data-line` attributes and adds `.kirari-line-highlighted` to selected lines.
109
+ Malformed groups and groups inside quoted metadata values are ignored.
110
+ Metadata is read from `code.data.meta`, as supplied by `remark-rehype` and MDX.
111
+
112
+ Provide your own line highlight styling:
113
+
114
+ ```css
115
+ .kirari-line-highlighted {
116
+ background: var(--kirari-line-background, #7452bd18);
117
+ }
118
+ ```
119
+
120
+ Use `lines: true` to wrap every highlighted block's lines even without metadata.
121
+ The `lineClassName` callback can append your own classes:
122
+
123
+ ```ts
124
+ import type { Options } from '@vortexlabs/kirari-rehype'
125
+
126
+ const options = {
127
+ languages: [javascript],
128
+ lines: true,
129
+ lineClassName: ({ number }) => number === 1 ? 'opening-line' : undefined,
130
+ } satisfies Options
131
+ ```
132
+
133
+ Line endings remain outside the wrappers. Empty and trailing lines are
134
+ preserved. Callback offsets are UTF-16 code units into the code block's source;
135
+ `start` is inclusive and `end` is exclusive and excludes the line ending.
136
+ Token class callbacks receive each original token once, even when a token spans
137
+ multiple lines.
138
+
139
+ ## Options
140
+
141
+ The default export is the plugin. `Options` is its exported TypeScript type.
142
+
143
+ | Option | Default | Behavior |
144
+ | --- | --- | --- |
145
+ | `languages` | `[]` | Explicit readonly list of Kirari language definitions. |
146
+ | `classes` | None | Token category to extra CSS class map, as in Kirari's `RenderOptions`. |
147
+ | `className(token)` | None | Extra CSS classes for each original token. |
148
+ | `lines` | `false` | Wrap every line of a highlighted block. Valid line metadata also enables wrappers. |
149
+ | `lineClassName({ number, start, end })` | None | Extra classes for a line; called only when line wrappers are enabled. |
150
+
151
+ Custom classes append to the standard Kirari classes. Plain `text` tokens remain
152
+ text nodes unless they receive a custom class. Source strings become text node
153
+ values, and classes become element properties; the downstream serializer or
154
+ MDX compiler handles escaping.
155
+
156
+ ## Pipeline order
157
+
158
+ Run the plugin after `remark-rehype`, when the document has become an HTML tree,
159
+ and before `rehype-stringify`. In MDX, use `rehypePlugins`, not `remarkPlugins`.
160
+ Configure one highlighter for a block; this plugin skips existing markup rather
161
+ than removing another plugin's spans.
162
+
163
+ If your pipeline sanitizes HTML, run `rehype-sanitize` before `rehypeKirari` and
164
+ preserve the `language-*` classes needed for lookup. If sanitization runs after
165
+ highlighting instead, its schema must allow Kirari's span classes, `.kirari` on
166
+ `pre`, and `data-line` when lines are enabled.
167
+
168
+ This plugin does not sanitize the surrounding document or change how your
169
+ pipeline handles raw HTML or MDX expressions. Language definitions and callbacks
170
+ remain trusted application code.
171
+
172
+ ## License
173
+
174
+ [MIT](LICENSE) © 2026 [Ugi](https://ugi.dev)
@@ -0,0 +1,13 @@
1
+ import type { Root } from 'hast';
2
+ import type { Language, RenderOptions } from '@vortexlabs/kirari/core';
3
+ export interface Options extends RenderOptions {
4
+ readonly languages?: readonly Language[];
5
+ readonly lines?: boolean;
6
+ readonly lineClassName?: (line: {
7
+ readonly number: number;
8
+ readonly start: number;
9
+ readonly end: number;
10
+ }) => string | undefined;
11
+ }
12
+ declare const rehypeKirari: (options?: Options) => (tree: Root) => undefined;
13
+ export default rehypeKirari;
package/dist/index.js ADDED
@@ -0,0 +1,149 @@
1
+ import { tokenize } from '@vortexlabs/kirari/core';
2
+ import { createRegistry } from '@vortexlabs/kirari/registry';
3
+ const classes = (value) => {
4
+ return (Array.isArray(value) ? value : [value])
5
+ .filter((item) => typeof item === 'string' || typeof item === 'number')
6
+ .flatMap(item => String(item).split(/[\t\n\f\r ]+/).filter(Boolean));
7
+ };
8
+ const lineRanges = (meta) => {
9
+ if (typeof meta !== 'string')
10
+ return [];
11
+ const ranges = [];
12
+ let quote = '';
13
+ let start = -1;
14
+ let depth = 0;
15
+ for (let index = 0; index < meta.length; index++) {
16
+ const char = meta[index];
17
+ if (char === '\\') {
18
+ index++;
19
+ start = -1;
20
+ }
21
+ else if (quote) {
22
+ if (char === quote)
23
+ quote = '';
24
+ }
25
+ else if (char === '"' || char === "'") {
26
+ quote = char;
27
+ start = -1;
28
+ }
29
+ else if (char === '{') {
30
+ depth++;
31
+ start = depth === 1 && (index === 0 || /\s/.test(meta[index - 1])) ? index + 1 : -1;
32
+ }
33
+ else if (char === '}') {
34
+ depth = Math.max(0, depth - 1);
35
+ if (depth !== 0 || start < 0)
36
+ continue;
37
+ const body = meta.slice(start, index);
38
+ if ((index + 1 === meta.length || /\s/.test(meta[index + 1]))
39
+ && /^\s*[1-9]\d*(?:\s*-\s*[1-9]\d*)?(?:\s*,\s*[1-9]\d*(?:\s*-\s*[1-9]\d*)?)*\s*$/.test(body)) {
40
+ const parsed = body.split(',').map(part => {
41
+ const [first, last = first] = part.split('-').map(Number);
42
+ return [first, last];
43
+ });
44
+ if (parsed.every(([first, last]) => Number.isSafeInteger(first) && Number.isSafeInteger(last) && first <= last)) {
45
+ for (const range of parsed)
46
+ ranges.push(range);
47
+ }
48
+ }
49
+ start = -1;
50
+ }
51
+ else if (start >= 0 && !/[\d\s,-]/.test(char)) {
52
+ start = -1;
53
+ }
54
+ }
55
+ return ranges.sort((left, right) => left[0] - right[0]);
56
+ };
57
+ const tokenNodes = (source, tokens, options, ranges) => {
58
+ let offset = 0;
59
+ for (const token of tokens) {
60
+ if (token.start !== offset || token.end !== offset + token.value.length
61
+ || token.value !== source.slice(token.start, token.end)) {
62
+ throw new RangeError('Kirari: tokens must cover the original code contiguously');
63
+ }
64
+ offset = token.end;
65
+ }
66
+ if (offset !== source.length)
67
+ throw new RangeError('Kirari: tokens must cover the original code contiguously');
68
+ // Resolve callbacks once before any multiline token is split into line spans.
69
+ const tokenClasses = tokens.map(token => {
70
+ const mapped = options.classes && Object.hasOwn(options.classes, token.type) ? options.classes[token.type] : undefined;
71
+ const extra = [mapped, options.className?.(token)].filter(Boolean).join(' ');
72
+ return token.type === 'text' && !extra ? [] : classes(`kirari-${token.type}${extra ? ` ${extra}` : ''}`);
73
+ });
74
+ let tokenIndex = 0;
75
+ const rangeNodes = (start, end) => {
76
+ const nodes = [];
77
+ while (start < end) {
78
+ while (tokens[tokenIndex].end <= start)
79
+ tokenIndex++;
80
+ const token = tokens[tokenIndex];
81
+ const stop = Math.min(end, token.end);
82
+ const text = { type: 'text', value: source.slice(start, stop) };
83
+ const className = tokenClasses[tokenIndex];
84
+ nodes.push(className.length ? { type: 'element', tagName: 'span', properties: { className }, children: [text] } : text);
85
+ start = stop;
86
+ }
87
+ return nodes;
88
+ };
89
+ if (!options.lines && ranges.length === 0)
90
+ return rangeNodes(0, source.length);
91
+ const nodes = [];
92
+ const newline = /\r\n|\r|\n/g;
93
+ let start = 0;
94
+ let number = 1;
95
+ let rangeIndex = 0;
96
+ do {
97
+ const match = newline.exec(source);
98
+ const end = match?.index ?? source.length;
99
+ const className = ['kirari-line', ...classes(options.lineClassName?.({ number, start, end }))];
100
+ while (ranges[rangeIndex] && ranges[rangeIndex][1] < number)
101
+ rangeIndex++;
102
+ if (ranges[rangeIndex] && ranges[rangeIndex][0] <= number)
103
+ className.push('kirari-line-highlighted');
104
+ nodes.push({ type: 'element', tagName: 'span', properties: { className, dataLine: number }, children: rangeNodes(start, end) });
105
+ if (!match)
106
+ break;
107
+ nodes.push({ type: 'text', value: match[0] });
108
+ start = end + match[0].length;
109
+ number++;
110
+ } while (true);
111
+ return nodes;
112
+ };
113
+ const rehypeKirari = (options = {}) => {
114
+ const registry = createRegistry(options.languages ?? []);
115
+ return tree => {
116
+ const parents = [tree];
117
+ while (parents.length) {
118
+ const parent = parents.pop();
119
+ for (const child of parent.children) {
120
+ if (child.type === 'element' && child.tagName === 'pre') {
121
+ const preClasses = classes(child.properties.className);
122
+ if (!preClasses.some(name => name === 'no-highlight' || name === 'nohighlight')) {
123
+ for (const code of child.children) {
124
+ if (code.type !== 'element' || code.tagName !== 'code' || code.children.some(node => node.type !== 'text'))
125
+ continue;
126
+ const codeClasses = classes(code.properties.className);
127
+ if (codeClasses.some(name => name === 'no-highlight' || name === 'nohighlight'))
128
+ continue;
129
+ const language = codeClasses.filter(name => name.startsWith('language-')).map(name => registry.resolve(name.slice(9))).find(Boolean);
130
+ if (!language)
131
+ continue;
132
+ const source = code.children.map(node => node.type === 'text' ? node.value : '').join('');
133
+ const meta = code.data && 'meta' in code.data ? code.data.meta : undefined;
134
+ const children = tokenNodes(source, tokenize(source, language), options, lineRanges(meta));
135
+ code.children = children;
136
+ child.properties.className = [...new Set([...preClasses, 'kirari'])];
137
+ }
138
+ }
139
+ continue;
140
+ }
141
+ // MDX JSX nodes can contain normal HAST children without being elements themselves.
142
+ if ('children' in child && Array.isArray(child.children))
143
+ parents.push(child);
144
+ }
145
+ }
146
+ return undefined;
147
+ };
148
+ };
149
+ export default rehypeKirari;
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@vortexlabs/kirari-rehype",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Kirari syntax highlighting for Rehype and MDX.",
5
+ "keywords": [
6
+ "rehype",
7
+ "rehype-plugin",
8
+ "mdx",
9
+ "syntax-highlighting"
10
+ ],
11
+ "author": "Ugi (https://ugi.dev)",
12
+ "license": "MIT",
13
+ "homepage": "https://kirari.vortex.design",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/vortexkit/kirari.git",
17
+ "directory": "packages/rehype"
18
+ },
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "test": "bun test ./test",
36
+ "typecheck": "tsc --noEmit",
37
+ "check": "bun run typecheck && bun run test && bun run build && bun test ./checks",
38
+ "prepublishOnly": "bun run build"
39
+ },
40
+ "dependencies": {
41
+ "@types/hast": "^3.0.5"
42
+ },
43
+ "peerDependencies": {
44
+ "@vortexlabs/kirari": "^0.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "@mdx-js/mdx": "^3.1.1",
48
+ "@types/react": "^19.3.0",
49
+ "@types/react-dom": "^19.3.0",
50
+ "@vortexlabs/kirari": "0.1.0",
51
+ "react": "^19.3.1",
52
+ "react-dom": "^19.3.1",
53
+ "rehype-sanitize": "^6.0.0",
54
+ "rehype-stringify": "^10.0.1",
55
+ "remark-parse": "^11.0.0",
56
+ "remark-rehype": "^11.1.2",
57
+ "unified": "^11.0.5"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }