fractalpop 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 +21 -0
- package/README.md +232 -0
- package/dist/core.d.ts +110 -0
- package/dist/core.js +313 -0
- package/dist/full.d.ts +1 -0
- package/dist/full.js +356 -0
- package/dist/gpu.d.ts +59 -0
- package/dist/gpu.js +215 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +523 -0
- package/dist/lang/c.d.ts +5 -0
- package/dist/lang/c.js +90 -0
- package/dist/lang/cpp.d.ts +5 -0
- package/dist/lang/cpp.js +138 -0
- package/dist/lang/csharp.d.ts +5 -0
- package/dist/lang/csharp.js +148 -0
- package/dist/lang/css.d.ts +21 -0
- package/dist/lang/css.js +164 -0
- package/dist/lang/diff.d.ts +30 -0
- package/dist/lang/diff.js +18 -0
- package/dist/lang/dockerfile.d.ts +5 -0
- package/dist/lang/dockerfile.js +60 -0
- package/dist/lang/go.d.ts +5 -0
- package/dist/lang/go.js +91 -0
- package/dist/lang/graphql.d.ts +5 -0
- package/dist/lang/graphql.js +66 -0
- package/dist/lang/hcl.d.ts +5 -0
- package/dist/lang/hcl.js +51 -0
- package/dist/lang/html.d.ts +12 -0
- package/dist/lang/html.js +168 -0
- package/dist/lang/java.d.ts +5 -0
- package/dist/lang/java.js +96 -0
- package/dist/lang/javascript.d.ts +5 -0
- package/dist/lang/javascript.js +0 -0
- package/dist/lang/json.d.ts +5 -0
- package/dist/lang/json.js +47 -0
- package/dist/lang/kotlin.d.ts +5 -0
- package/dist/lang/kotlin.js +113 -0
- package/dist/lang/lua.d.ts +5 -0
- package/dist/lang/lua.js +65 -0
- package/dist/lang/markdown.d.ts +12 -0
- package/dist/lang/markdown.js +172 -0
- package/dist/lang/php.d.ts +5 -0
- package/dist/lang/php.js +129 -0
- package/dist/lang/plaintext.d.ts +7 -0
- package/dist/lang/plaintext.js +3 -0
- package/dist/lang/powershell.d.ts +5 -0
- package/dist/lang/powershell.js +74 -0
- package/dist/lang/python.d.ts +5 -0
- package/dist/lang/python.js +75 -0
- package/dist/lang/ruby.d.ts +5 -0
- package/dist/lang/ruby.js +84 -0
- package/dist/lang/rust.d.ts +5 -0
- package/dist/lang/rust.js +95 -0
- package/dist/lang/sass.d.ts +15 -0
- package/dist/lang/sass.js +201 -0
- package/dist/lang/scss.d.ts +14 -0
- package/dist/lang/scss.js +105 -0
- package/dist/lang/shell.d.ts +5 -0
- package/dist/lang/shell.js +64 -0
- package/dist/lang/sql.d.ts +5 -0
- package/dist/lang/sql.js +129 -0
- package/dist/lang/svelte.d.ts +15 -0
- package/dist/lang/svelte.js +0 -0
- package/dist/lang/swift.d.ts +5 -0
- package/dist/lang/swift.js +125 -0
- package/dist/lang/toml.d.ts +5 -0
- package/dist/lang/toml.js +45 -0
- package/dist/lang/typescript.d.ts +5 -0
- package/dist/lang/typescript.js +0 -0
- package/dist/lang/yaml.d.ts +5 -0
- package/dist/lang/yaml.js +64 -0
- package/dist/lang/zig.d.ts +5 -0
- package/dist/lang/zig.js +133 -0
- package/dist/lang.d.ts +30 -0
- package/dist/lang.js +345 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FractalMandala
|
|
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
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# fractalpop
|
|
2
|
+
|
|
3
|
+
`fractalpop` is the engine. It highlights a string of source code and returns
|
|
4
|
+
an HTML string — no DOM, no runtime dependencies. Use it directly on any
|
|
5
|
+
platform, or through one of the adapter packages.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install fractalpop
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
Call `highlight()` with your code and a language. The result is a string of
|
|
16
|
+
nested `<span>` elements that you drop into a `<pre><code>` block.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { highlight } from 'fractalpop'
|
|
20
|
+
|
|
21
|
+
const html = highlight('const ready = true', { lang: 'ts' })
|
|
22
|
+
// <span class="fp__line"><span class="fp__token--keyword" style="color:var(--fp-keyword)">const</span> …
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The output is HTML-encoded, so it is safe to inject with `innerHTML`,
|
|
26
|
+
`{@html}`, or `dangerouslySetInnerHTML`.
|
|
27
|
+
|
|
28
|
+
## `highlight(code, options)`
|
|
29
|
+
|
|
30
|
+
`highlight` takes the source string and an options object. It returns an HTML string.
|
|
31
|
+
|
|
32
|
+
| Option | Type | Default | Purpose |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `lang` | `string` | `'typescript'` | Language id, extension, or alias (see [Languages](#languages)). |
|
|
35
|
+
| `cx` | `Partial<Record<TokenType, string>>` | — | Extra class name to add per token type. |
|
|
36
|
+
| `mark` | `(token: MarkToken) => void` | — | Mutate a single token before it renders. |
|
|
37
|
+
| `markLine` | `(line: MarkLine) => void` | — | Mutate a whole line before it renders. |
|
|
38
|
+
|
|
39
|
+
`cx`, `mark`, and `markLine` are the only customization hooks, and they control
|
|
40
|
+
performance. When you pass **none** of them, `render` takes a fast path: it
|
|
41
|
+
concatenates strings with no per-token object allocation. Passing any hook
|
|
42
|
+
switches to the slower path that builds mutable token and line objects so the
|
|
43
|
+
hooks can run. If you do not need customization, pass no hooks.
|
|
44
|
+
|
|
45
|
+
### Emphasis with `cx`
|
|
46
|
+
|
|
47
|
+
`cx` appends a class to every token of a given type. This is the Tailwind-
|
|
48
|
+
friendly way to emphasize tokens without writing selectors.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
highlight(code, {
|
|
52
|
+
lang: 'sass',
|
|
53
|
+
cx: { keyword: 'font-bold', comment: 'italic opacity-70' },
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Line highlighting with `markLine`
|
|
58
|
+
|
|
59
|
+
Each line exposes its zero-based `index`. Add a class when the line is one you
|
|
60
|
+
want to highlight.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const active = new Set([2, 3]) // 1-based line numbers
|
|
64
|
+
|
|
65
|
+
highlight(code, {
|
|
66
|
+
lang: 'ts',
|
|
67
|
+
markLine(line) {
|
|
68
|
+
if (active.has(line.index + 1)) line.className += ' fp__line--highlighted'
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Theming
|
|
74
|
+
|
|
75
|
+
fractalpop does not ship colors. Every token renders with
|
|
76
|
+
`class="fp__token--<type>"` and an inline `style="color:var(--fp-<type>)"`, so a
|
|
77
|
+
theme is a block of CSS variables you set on any ancestor.
|
|
78
|
+
|
|
79
|
+
```css
|
|
80
|
+
:root {
|
|
81
|
+
--fp-identifier: #354150;
|
|
82
|
+
--fp-keyword: #f47067;
|
|
83
|
+
--fp-string: #00a99a;
|
|
84
|
+
--fp-class: #8d85ff;
|
|
85
|
+
--fp-property: #4e8fdf;
|
|
86
|
+
--fp-entity: #665ac7;
|
|
87
|
+
--fp-jsxliterals: #bf7db6;
|
|
88
|
+
--fp-sign: #8996a3;
|
|
89
|
+
--fp-comment: #a19595;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.fp__line--highlighted {
|
|
93
|
+
background: #fff8c5;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For dark mode, redefine the same variables under a selector or media query — for
|
|
98
|
+
example `:root[data-theme='dark']` or `@media (prefers-color-scheme: dark)`.
|
|
99
|
+
|
|
100
|
+
### Token types
|
|
101
|
+
|
|
102
|
+
Token types are a small, fixed list. Keeping it stable keeps themes portable.
|
|
103
|
+
|
|
104
|
+
| Type | CSS variable | Typical meaning |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| `identifier` | `--fp-identifier` | Plain identifiers and default text. |
|
|
107
|
+
| `keyword` | `--fp-keyword` | Language keywords. |
|
|
108
|
+
| `string` | `--fp-string` | Strings, regex literals, template text. |
|
|
109
|
+
| `class` | `--fp-class` | Capitalized names, numbers, `null`, types. |
|
|
110
|
+
| `property` | `--fp-property` | Object/CSS properties, Sass `$variables`. |
|
|
111
|
+
| `entity` | `--fp-entity` | Function and mixin names. |
|
|
112
|
+
| `jsxliterals` | `--fp-jsxliterals` | JSX text between tags. |
|
|
113
|
+
| `sign` | `--fp-sign` | Punctuation and operators. |
|
|
114
|
+
| `comment` | `--fp-comment` | Comments. |
|
|
115
|
+
|
|
116
|
+
Two more types, `break` (newlines) and `space` (horizontal whitespace), are
|
|
117
|
+
preserved for faithful output but carry no color.
|
|
118
|
+
|
|
119
|
+
## Languages
|
|
120
|
+
|
|
121
|
+
fractalpop ships metadata and configs for **32 languages**, but the default
|
|
122
|
+
entry only **registers** TypeScript and plaintext — the rest stay tree-
|
|
123
|
+
shakeable. Register what you need yourself, or import `fractalpop/full` to
|
|
124
|
+
register all 32 up front. Resolve a name, file extension, or alias to a
|
|
125
|
+
canonical id with `lang()`.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { lang, findLanguage, allLanguages } from 'fractalpop/lang'
|
|
129
|
+
|
|
130
|
+
lang('tsx') // 'typescript'
|
|
131
|
+
lang('.scss') // 'scss' (metadata is known; register the config to highlight)
|
|
132
|
+
lang('yml') // 'yaml'
|
|
133
|
+
lang('nope') // undefined
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The four languages built with the most care are the ones fractalpop exists for:
|
|
137
|
+
|
|
138
|
+
- `sass` — indented Sass (no braces, no semicolons).
|
|
139
|
+
- `scss` — the brace dialect, sharing Sass's value coloring.
|
|
140
|
+
- `svelte` — the composite: `<script lang="ts">` + markup + `<style lang="sass">`.
|
|
141
|
+
- `typescript` / `javascript` — a full JS/JSX/TS runtime (regex literals,
|
|
142
|
+
template interpolation, JSX, and the TS-generic-vs-JSX distinction).
|
|
143
|
+
|
|
144
|
+
The rest is the general pack: `css`, `html`, `markdown`, `diff`, `plaintext`,
|
|
145
|
+
`c`, `cpp`, `csharp`, `go`, `java`, `rust`, `json`, `shell`, `sql`, `yaml`,
|
|
146
|
+
`toml`, `python`, `ruby`, `php`, `kotlin`, `swift`, `lua`, `graphql`, `hcl`,
|
|
147
|
+
`dockerfile`, `powershell`, `zig`.
|
|
148
|
+
|
|
149
|
+
## Subpath exports
|
|
150
|
+
|
|
151
|
+
The package exposes several entries so you only bundle what you need.
|
|
152
|
+
|
|
153
|
+
| Import | Exposes |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `fractalpop` | `highlight`, registry utilities, and the option/token types. **Only TypeScript and plaintext are registered by default.** |
|
|
156
|
+
| `fractalpop/full` | Everything from `fractalpop`, with all 32 languages registered. |
|
|
157
|
+
| `fractalpop/core` | `parse`, `tokenize`, `render` (→ string), `generate` (→ AST). |
|
|
158
|
+
| `fractalpop/lang` | `lang`, `findLanguage`, `allLanguages`. |
|
|
159
|
+
| `fractalpop/lang/<id>` | A single language config (tree-shakeable). |
|
|
160
|
+
| `fractalpop/gpu` | Async WebGPU highlighting via optional peer `gpu-lexer`. |
|
|
161
|
+
|
|
162
|
+
`generate` returns a hast-like AST, which is what a unified/rehype pipeline
|
|
163
|
+
consumes. `render` returns the HTML string that `highlight` wraps.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { parse, generate } from 'fractalpop/core'
|
|
167
|
+
import { lang, getLanguageConfig } from 'fractalpop/full'
|
|
168
|
+
|
|
169
|
+
const parsed = parse(code, getLanguageConfig(lang('sass')!))
|
|
170
|
+
const tree = generate(parsed) // element/text nodes, one per line and token
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Registry utilities
|
|
174
|
+
|
|
175
|
+
The default `fractalpop` entry only registers TypeScript and plaintext. Use
|
|
176
|
+
these utilities to control which languages are available at runtime.
|
|
177
|
+
|
|
178
|
+
The registry is a single module-level singleton shared by every entry point
|
|
179
|
+
(`fractalpop/full` re-exports and mutates the same core as `fractalpop`), so a
|
|
180
|
+
registration made through one import path is visible to all others in the same
|
|
181
|
+
app.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import {
|
|
185
|
+
registerLanguage,
|
|
186
|
+
setDefaults,
|
|
187
|
+
importDefaults,
|
|
188
|
+
canonicalizeLang,
|
|
189
|
+
getRegisteredLanguages,
|
|
190
|
+
} from 'fractalpop'
|
|
191
|
+
|
|
192
|
+
registerLanguage({ id: 'python', extension: 'py', aliases: ['python3'] }, pythonConfig)
|
|
193
|
+
setDefaults({ python: pythonConfig, rust: rustConfig })
|
|
194
|
+
await importDefaults(['python', 'rust'])
|
|
195
|
+
canonicalizeLang('tsx') // 'typescript'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Function | Purpose |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| `registerLanguage(language, config?)` | Register one language, optionally with its parse config. |
|
|
201
|
+
| `setDefaults(languagesOrConfigs)` | Replace the default set. Accepts `Language[]` or `Record<string, ParseOptions>`. |
|
|
202
|
+
| `importDefaults(ids)` | Asynchronously import and register languages by id. |
|
|
203
|
+
| `canonicalizeLang(name)` | Resolve a name/alias/extension to the canonical id. |
|
|
204
|
+
| `getRegisteredLanguages()` | List currently registered languages. |
|
|
205
|
+
|
|
206
|
+
## Related Packages
|
|
207
|
+
|
|
208
|
+
- [@fractalpop/svelte](https://www.npmjs.com/package/@fractalpop/svelte) — Svelte 5 components and action
|
|
209
|
+
- [@fractalpop/mdsvex](https://www.npmjs.com/package/@fractalpop/mdsvex) — Highlighter hook for MDSveX in SvelteKit
|
|
210
|
+
- [@fractalpop/remark](https://www.npmjs.com/package/@fractalpop/remark) — Remark plugin for Markdown & MDX
|
|
211
|
+
|
|
212
|
+
## Benchmarks
|
|
213
|
+
|
|
214
|
+
fractalpop, Sugar High, PrismJS, and highlight.js highlighting the same generated TypeScript files:
|
|
215
|
+
|
|
216
|
+
<!-- benchmark:start -->
|
|
217
|
+
Measured 2026-09-12 with Node v24.19.0, darwin arm64, Apple M3 Pro.
|
|
218
|
+
|
|
219
|
+
| TypeScript | fractalpop 0.1.0 | Sugar High 2.4.0 | PrismJS 1.30.0 | highlight.js 11.12.0 |
|
|
220
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
221
|
+
| Minified (KiB) | 12.35 | 27.29 | 14.57 | 29.49 |
|
|
222
|
+
| Gzip (KiB) | 5.28 | 10.09 | 5.57 | 11.28 |
|
|
223
|
+
| 11 KiB | 1.87 | 1.91 | 1.42 | 2.39 |
|
|
224
|
+
| 100 KiB | 19.49 | 19.61 | 14.05 | 23.57 |
|
|
225
|
+
| 500 KiB | 98.13 | 100.04 | 90.45 | 117.98 |
|
|
226
|
+
|
|
227
|
+
Median milliseconds per file; lower is better. 5 timed samples after warmup.
|
|
228
|
+
Sizes are TypeScript-only browser bundles, minified with Bun; gzip uses level 9. Theme CSS is excluded.
|
|
229
|
+
Loading and initialization are excluded. Each library highlights the same generated TypeScript
|
|
230
|
+
into HTML using an explicit language. Grammars and HTML output differ; this is not a measure
|
|
231
|
+
of highlighting quality or browser rendering speed. Results vary by machine and workload.
|
|
232
|
+
<!-- benchmark:end -->
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fractalpop core — token model, line assembly, and rendering.
|
|
3
|
+
*
|
|
4
|
+
* Design: tokens are numeric `[type, value]` pairs during lexing (fast Set/array
|
|
5
|
+
* work); the string type names are resolved only at render. Output is an HTML
|
|
6
|
+
* string with no DOM required — so SSR and client produce identical markup.
|
|
7
|
+
*/
|
|
8
|
+
/** The stable, ordered token-type list. This list is the theming contract:
|
|
9
|
+
* each type maps to one CSS variable `--fp-<type>` and one class `fp__token--<type>`. */
|
|
10
|
+
declare const TokenTypes: readonly ["identifier", "keyword", "string", "class", "property", "entity", "jsxliterals", "sign", "comment", "break", "space"];
|
|
11
|
+
type TokenType = (typeof TokenTypes)[number];
|
|
12
|
+
/** A lexer emits these: `[numericType, value]`. */
|
|
13
|
+
type Token = [number, string];
|
|
14
|
+
declare const FractalPop: {
|
|
15
|
+
readonly TokenTypes: readonly ["identifier", "keyword", "string", "class", "property", "entity", "jsxliterals", "sign", "comment", "break", "space"];
|
|
16
|
+
readonly TokenMap: Map<"string" | "identifier" | "keyword" | "class" | "property" | "entity" | "jsxliterals" | "sign" | "comment" | "break" | "space", number>;
|
|
17
|
+
};
|
|
18
|
+
interface ParsedToken {
|
|
19
|
+
type: TokenType;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
interface ParsedLine {
|
|
23
|
+
index: number;
|
|
24
|
+
value: string;
|
|
25
|
+
tokens: ParsedToken[];
|
|
26
|
+
annotations: string[];
|
|
27
|
+
}
|
|
28
|
+
interface ParsedCode {
|
|
29
|
+
value: string;
|
|
30
|
+
lines: ParsedLine[];
|
|
31
|
+
}
|
|
32
|
+
/** Mutable token handed to `mark`. Hooks may change className/style/properties. */
|
|
33
|
+
interface MarkToken {
|
|
34
|
+
type: TokenType;
|
|
35
|
+
value: string;
|
|
36
|
+
className: string;
|
|
37
|
+
style: Record<string, string | number>;
|
|
38
|
+
properties: Record<string, string | number | boolean>;
|
|
39
|
+
}
|
|
40
|
+
/** Mutable line handed to `markLine`. Hooks may change className/style/properties. */
|
|
41
|
+
interface MarkLine {
|
|
42
|
+
index: number;
|
|
43
|
+
value: string;
|
|
44
|
+
tokens: ParsedToken[];
|
|
45
|
+
annotations: string[];
|
|
46
|
+
className: string;
|
|
47
|
+
style: Record<string, string | number>;
|
|
48
|
+
properties: Record<string, string | number | boolean>;
|
|
49
|
+
}
|
|
50
|
+
interface DisplayOptions {
|
|
51
|
+
/** Extra class name appended per token type, e.g. `{ keyword: 'font-bold' }`. */
|
|
52
|
+
cx?: Partial<Record<TokenType, string>>;
|
|
53
|
+
/** Mutate a single token before it renders. */
|
|
54
|
+
mark?: (token: MarkToken) => void;
|
|
55
|
+
/** Mutate a whole line before it renders (used for line highlighting). */
|
|
56
|
+
markLine?: (line: MarkLine) => void;
|
|
57
|
+
}
|
|
58
|
+
/** hast-like AST node (element or text). Consumed by the remark adapter. */
|
|
59
|
+
interface AstText {
|
|
60
|
+
type: 'text';
|
|
61
|
+
value: string;
|
|
62
|
+
}
|
|
63
|
+
interface AstElement {
|
|
64
|
+
type: 'element';
|
|
65
|
+
tagName: string;
|
|
66
|
+
tokenType?: TokenType;
|
|
67
|
+
children: Array<AstElement | AstText>;
|
|
68
|
+
properties: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
/** Build a hast-like AST (one element per line, one per token). For unified/hast pipelines. */
|
|
71
|
+
declare function generate(parsed: ParsedCode, options?: DisplayOptions): AstElement[];
|
|
72
|
+
/** Render parsed lines to an HTML string. Fast path when no display hooks are set. */
|
|
73
|
+
declare function render(parsed: ParsedCode, options?: DisplayOptions): string;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* fractalpop core — the general-purpose lexer and `parse`.
|
|
77
|
+
*
|
|
78
|
+
* A single left-to-right scan classifies keyword/string/comment/sign languages.
|
|
79
|
+
* Complex languages (CSS, Sass, Svelte) supply a `tokenize` override that runs
|
|
80
|
+
* this plain lexer and then post-processes the token array.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
interface ParseOptions {
|
|
84
|
+
keywords?: Set<string>;
|
|
85
|
+
typeKeywords?: Set<string>;
|
|
86
|
+
onCommentStart?: (curr: string, next: string, index: number, code: string) => number | boolean;
|
|
87
|
+
onCommentEnd?: (prev: string, curr: string, index: number, code: string, start: number) => number | boolean;
|
|
88
|
+
onLiteral?: (curr: string, index: number, code: string) => number | null | undefined;
|
|
89
|
+
onQuote?: (curr: string, index: number, code: string) => number | null | undefined;
|
|
90
|
+
quotedKeys?: boolean;
|
|
91
|
+
caseInsensitive?: boolean;
|
|
92
|
+
templateStrings?: boolean;
|
|
93
|
+
/** JS runtime: enable JSX tag parsing. */
|
|
94
|
+
jsx?: boolean;
|
|
95
|
+
/** JS runtime: enable regex-literal detection. */
|
|
96
|
+
regex?: boolean;
|
|
97
|
+
/** JS runtime: force TypeScript keyword set (else heuristic detection). */
|
|
98
|
+
typescript?: boolean;
|
|
99
|
+
/** Full tokenize override for complex languages. */
|
|
100
|
+
tokenize?: (code: string, options: ParseOptions) => Token[];
|
|
101
|
+
/** Per-line annotation hook (adds classes like `fp__line--<annotation>`). */
|
|
102
|
+
annotateLine?: (line: ParsedLine) => void;
|
|
103
|
+
}
|
|
104
|
+
/** General lexer. Returns `[numericType, value]` pairs. */
|
|
105
|
+
declare function tokenize(code: string, options?: ParseOptions): Token[];
|
|
106
|
+
/** Tokenize + assemble into lines, running any per-line annotation hook. */
|
|
107
|
+
declare function parse(code: string, options?: ParseOptions): ParsedCode;
|
|
108
|
+
|
|
109
|
+
export { FractalPop, generate, parse, render, tokenize };
|
|
110
|
+
export type { ParseOptions };
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fractalpop core — token model, line assembly, and rendering.
|
|
3
|
+
*
|
|
4
|
+
* Design: tokens are numeric `[type, value]` pairs during lexing (fast Set/array
|
|
5
|
+
* work); the string type names are resolved only at render. Output is an HTML
|
|
6
|
+
* string with no DOM required — so SSR and client produce identical markup.
|
|
7
|
+
*/ /** The stable, ordered token-type list. This list is the theming contract:
|
|
8
|
+
* each type maps to one CSS variable `--fp-<type>` and one class `fp__token--<type>`. */ const TokenTypes = [
|
|
9
|
+
'identifier',
|
|
10
|
+
'keyword',
|
|
11
|
+
'string',
|
|
12
|
+
'class',
|
|
13
|
+
'property',
|
|
14
|
+
'entity',
|
|
15
|
+
'jsxliterals',
|
|
16
|
+
'sign',
|
|
17
|
+
'comment',
|
|
18
|
+
'break',
|
|
19
|
+
'space'
|
|
20
|
+
];
|
|
21
|
+
// Numeric indices, used everywhere in the hot path.
|
|
22
|
+
const T_IDENTIFIER = 0;
|
|
23
|
+
const T_KEYWORD = 1;
|
|
24
|
+
const T_STRING = 2;
|
|
25
|
+
const T_CLASS = 3;
|
|
26
|
+
const T_PROPERTY = 4;
|
|
27
|
+
const T_SIGN = 7;
|
|
28
|
+
const T_COMMENT = 8;
|
|
29
|
+
const T_BREAK = 9;
|
|
30
|
+
const T_SPACE = 10;
|
|
31
|
+
const FractalPop = {
|
|
32
|
+
TokenTypes,
|
|
33
|
+
TokenMap: new Map(TokenTypes.map((type, index)=>[
|
|
34
|
+
type,
|
|
35
|
+
index
|
|
36
|
+
]))
|
|
37
|
+
};
|
|
38
|
+
/** Split a flat token stream into lines, handling embedded newlines. */ function assemble(value, tokens) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
let lineIndex = 0;
|
|
41
|
+
const lineTokens = [];
|
|
42
|
+
let lastWasBreak = false;
|
|
43
|
+
function flushLine(tokens) {
|
|
44
|
+
lines.push({
|
|
45
|
+
index: lineIndex++,
|
|
46
|
+
value: tokens.map(([, tokenValue])=>tokenValue).join(''),
|
|
47
|
+
tokens: tokens.map(([type, tokenValue])=>({
|
|
48
|
+
type: TokenTypes[type],
|
|
49
|
+
value: tokenValue
|
|
50
|
+
})),
|
|
51
|
+
annotations: []
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
for(let index = 0; index < tokens.length; index++){
|
|
55
|
+
const token = tokens[index];
|
|
56
|
+
const [type, tokenValue] = token;
|
|
57
|
+
if (type !== T_BREAK) {
|
|
58
|
+
if (tokenValue.includes('\n')) {
|
|
59
|
+
const values = tokenValue.split('\n');
|
|
60
|
+
for(let part = 0; part < values.length; part++){
|
|
61
|
+
lineTokens.push([
|
|
62
|
+
type,
|
|
63
|
+
values[part]
|
|
64
|
+
]);
|
|
65
|
+
if (part < values.length - 1) {
|
|
66
|
+
flushLine(lineTokens);
|
|
67
|
+
lineTokens.length = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
lineTokens.push(token);
|
|
72
|
+
}
|
|
73
|
+
lastWasBreak = false;
|
|
74
|
+
} else {
|
|
75
|
+
if (lastWasBreak) {
|
|
76
|
+
flushLine([]);
|
|
77
|
+
} else {
|
|
78
|
+
flushLine(lineTokens);
|
|
79
|
+
lineTokens.length = 0;
|
|
80
|
+
}
|
|
81
|
+
if (index === tokens.length - 1) flushLine([]);
|
|
82
|
+
lastWasBreak = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (lineTokens.length) flushLine(lineTokens);
|
|
86
|
+
return {
|
|
87
|
+
value,
|
|
88
|
+
lines
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function lineClassName(annotations) {
|
|
92
|
+
return `fp__line${annotations.map((annotation)=>` fp__line--${annotation}`).join('')}`;
|
|
93
|
+
}
|
|
94
|
+
function createLine(parsedLine, markLine) {
|
|
95
|
+
const line = {
|
|
96
|
+
index: parsedLine.index,
|
|
97
|
+
value: parsedLine.value,
|
|
98
|
+
tokens: parsedLine.tokens,
|
|
99
|
+
annotations: parsedLine.annotations,
|
|
100
|
+
className: lineClassName(parsedLine.annotations),
|
|
101
|
+
style: {},
|
|
102
|
+
properties: {}
|
|
103
|
+
};
|
|
104
|
+
markLine?.(line);
|
|
105
|
+
return line;
|
|
106
|
+
}
|
|
107
|
+
function createToken({ type, value }, cx, mark) {
|
|
108
|
+
const extraClassName = cx?.[type];
|
|
109
|
+
const token = {
|
|
110
|
+
type,
|
|
111
|
+
value,
|
|
112
|
+
className: `fp__token--${type}${extraClassName ? ` ${extraClassName}` : ''}`,
|
|
113
|
+
style: {
|
|
114
|
+
color: `var(--fp-${type})`
|
|
115
|
+
},
|
|
116
|
+
properties: {}
|
|
117
|
+
};
|
|
118
|
+
mark?.(token);
|
|
119
|
+
return token;
|
|
120
|
+
}
|
|
121
|
+
/** Build a hast-like AST (one element per line, one per token). For unified/hast pipelines. */ function generate(parsed, options) {
|
|
122
|
+
const cx = options?.cx;
|
|
123
|
+
const mark = options?.mark;
|
|
124
|
+
const markLine = options?.markLine;
|
|
125
|
+
return parsed.lines.map((parsedLine)=>{
|
|
126
|
+
const line = createLine(parsedLine, markLine);
|
|
127
|
+
return {
|
|
128
|
+
type: 'element',
|
|
129
|
+
tagName: 'span',
|
|
130
|
+
children: parsedLine.tokens.map((parsedToken)=>{
|
|
131
|
+
const token = createToken(parsedToken, cx, mark);
|
|
132
|
+
return {
|
|
133
|
+
type: 'element',
|
|
134
|
+
tokenType: token.type,
|
|
135
|
+
tagName: 'span',
|
|
136
|
+
children: [
|
|
137
|
+
{
|
|
138
|
+
type: 'text',
|
|
139
|
+
value: token.value
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
properties: {
|
|
143
|
+
...token.properties,
|
|
144
|
+
className: token.className,
|
|
145
|
+
style: token.style
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}),
|
|
149
|
+
properties: {
|
|
150
|
+
...line.properties,
|
|
151
|
+
className: line.className,
|
|
152
|
+
style: line.style
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Render parsed lines to an HTML string. Fast path when no display hooks are set. */ function render(parsed, options) {
|
|
158
|
+
const cx = options?.cx;
|
|
159
|
+
const mark = options?.mark;
|
|
160
|
+
const markLine = options?.markLine;
|
|
161
|
+
// Fast path: no per-token object allocation, straight string concat.
|
|
162
|
+
if (!cx && !mark && !markLine) {
|
|
163
|
+
return parsed.lines.map((line)=>{
|
|
164
|
+
const className = lineClassName(line.annotations);
|
|
165
|
+
const children = line.tokens.map(({ type, value })=>`<span class="fp__token--${type}" style="color:var(--fp-${type})">${encode(value)}</span>`).join('');
|
|
166
|
+
return `<span class="${encode(className)}">${children}</span>`;
|
|
167
|
+
}).join('\n');
|
|
168
|
+
}
|
|
169
|
+
// Hook path: build mutable objects and run cx/mark/markLine.
|
|
170
|
+
return parsed.lines.map((parsedLine)=>{
|
|
171
|
+
const line = createLine(parsedLine, markLine);
|
|
172
|
+
const children = parsedLine.tokens.map((parsedToken)=>{
|
|
173
|
+
const token = createToken(parsedToken, cx, mark);
|
|
174
|
+
return `<span ${attributes({
|
|
175
|
+
...token.properties,
|
|
176
|
+
className: token.className,
|
|
177
|
+
style: token.style
|
|
178
|
+
})}>${encode(token.value)}</span>`;
|
|
179
|
+
}).join('');
|
|
180
|
+
return `<span ${attributes({
|
|
181
|
+
...line.properties,
|
|
182
|
+
className: line.className,
|
|
183
|
+
style: line.style
|
|
184
|
+
})}>${children}</span>`;
|
|
185
|
+
}).join('\n');
|
|
186
|
+
}
|
|
187
|
+
const entities = {
|
|
188
|
+
'&': '&',
|
|
189
|
+
'<': '<',
|
|
190
|
+
'>': '>',
|
|
191
|
+
'"': '"',
|
|
192
|
+
"'": '''
|
|
193
|
+
};
|
|
194
|
+
const encode = (value)=>value.replace(/[&<>"']/g, (character)=>entities[character]);
|
|
195
|
+
function attributes(values) {
|
|
196
|
+
const styleValue = values.style ?? {};
|
|
197
|
+
const style = Object.entries(styleValue).map(([key, value])=>`${key.replace(/[A-Z]/g, (match)=>`-${match.toLowerCase()}`)}:${value}`).join(';');
|
|
198
|
+
const properties = Object.entries(values).filter(([key, value])=>/^[\w:-]+$/.test(key) && key !== 'className' && key !== 'style' && value !== false && value != null).map(([key, value])=>value === true ? key : `${key}="${encode(String(value))}"`).join(' ');
|
|
199
|
+
const className = values.className || '';
|
|
200
|
+
return `class="${encode(className)}"${style ? ` style="${encode(style)}"` : ''}${properties ? ` ${properties}` : ''}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const signs = new Set('+-*/%=!&|^~?:.,;()[]{}<>#@\\'.split(''));
|
|
204
|
+
const noComment = ()=>0;
|
|
205
|
+
const isWord = (value)=>value === '_' || value === '$' || /[\p{L}\p{N}]/u.test(value);
|
|
206
|
+
function isQuotedKey(code, index) {
|
|
207
|
+
while(index < code.length && /\s/.test(code[index]))index++;
|
|
208
|
+
return code[index] === ':';
|
|
209
|
+
}
|
|
210
|
+
/** General lexer. Returns `[numericType, value]` pairs. */ function tokenize(code, options) {
|
|
211
|
+
if (typeof options?.tokenize === 'function') return options.tokenize(code, options);
|
|
212
|
+
const keywords = options?.keywords || new Set();
|
|
213
|
+
const typeKeywords = options?.typeKeywords || new Set();
|
|
214
|
+
const onCommentStart = options?.onCommentStart || noComment;
|
|
215
|
+
const onCommentEnd = options?.onCommentEnd || noComment;
|
|
216
|
+
const normalize = options?.caseInsensitive ? (value)=>value.toLowerCase() : (value)=>value;
|
|
217
|
+
const tokens = [];
|
|
218
|
+
let lastSignificant = '';
|
|
219
|
+
function append(type, value) {
|
|
220
|
+
if (!value) return;
|
|
221
|
+
tokens.push([
|
|
222
|
+
type,
|
|
223
|
+
value
|
|
224
|
+
]);
|
|
225
|
+
if (type !== T_SPACE && type !== T_BREAK) lastSignificant = value;
|
|
226
|
+
}
|
|
227
|
+
for(let i = 0; i < code.length;){
|
|
228
|
+
const curr = code[i];
|
|
229
|
+
const next = code[i + 1] ?? '';
|
|
230
|
+
const commentType = onCommentStart(curr, next, i, code);
|
|
231
|
+
if (commentType) {
|
|
232
|
+
const start = i++;
|
|
233
|
+
while(i < code.length){
|
|
234
|
+
if (onCommentEnd(code[i - 1], code[i], i, code, start) == commentType) {
|
|
235
|
+
i++;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
i++;
|
|
239
|
+
}
|
|
240
|
+
append(T_COMMENT, code.slice(start, i));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const literalLength = options?.onLiteral?.(curr, i, code);
|
|
244
|
+
if (literalLength) {
|
|
245
|
+
append(T_STRING, code.slice(i, i + literalLength));
|
|
246
|
+
i += literalLength;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (typeof options?.onQuote === 'function' && curr === "'") {
|
|
250
|
+
const length = options.onQuote(curr, i, code);
|
|
251
|
+
if (typeof length === 'number' && length >= 1) {
|
|
252
|
+
append(T_IDENTIFIER, code.slice(i, i + length));
|
|
253
|
+
i += length;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (curr === '"' || curr === "'" || options?.templateStrings && curr === '`') {
|
|
258
|
+
const quote = curr;
|
|
259
|
+
const start = i++;
|
|
260
|
+
while(i < code.length){
|
|
261
|
+
if (code[i] === quote && code[i - 1] !== '\\') {
|
|
262
|
+
i++;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
i++;
|
|
266
|
+
}
|
|
267
|
+
const value = code.slice(start, i);
|
|
268
|
+
append(options?.quotedKeys && isQuotedKey(code, i) ? T_PROPERTY : T_STRING, value);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (curr === '\n') {
|
|
272
|
+
append(T_BREAK, curr);
|
|
273
|
+
i++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (/[^\S\r\n]/.test(curr)) {
|
|
277
|
+
const start = i++;
|
|
278
|
+
while(i < code.length && /[^\S\r\n]/.test(code[i]))i++;
|
|
279
|
+
append(T_SPACE, code.slice(start, i));
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (isWord(curr)) {
|
|
283
|
+
const start = i++;
|
|
284
|
+
while(i < code.length && isWord(code[i]))i++;
|
|
285
|
+
if (/^\d/.test(curr) && code[i] === '.' && /\d/.test(code[i + 1] || '')) {
|
|
286
|
+
i++;
|
|
287
|
+
while(i < code.length && isWord(code[i]))i++;
|
|
288
|
+
}
|
|
289
|
+
const value = code.slice(start, i);
|
|
290
|
+
const normalized = normalize(value);
|
|
291
|
+
const type = typeKeywords.has(normalized) ? T_CLASS : keywords.has(normalized) ? T_KEYWORD : lastSignificant === '.' ? T_PROPERTY : /^\d/.test(value) || value === 'null' || /^\p{Lu}/u.test(value) ? T_CLASS : T_IDENTIFIER;
|
|
292
|
+
append(type, value);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (signs.has(curr)) {
|
|
296
|
+
append(T_SIGN, curr);
|
|
297
|
+
i++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
append(T_STRING, curr);
|
|
301
|
+
i++;
|
|
302
|
+
}
|
|
303
|
+
return tokens;
|
|
304
|
+
}
|
|
305
|
+
/** Tokenize + assemble into lines, running any per-line annotation hook. */ function parse(code, options) {
|
|
306
|
+
const parsed = assemble(code, tokenize(code, options));
|
|
307
|
+
if (options?.annotateLine) {
|
|
308
|
+
for (const line of parsed.lines)options.annotateLine(line);
|
|
309
|
+
}
|
|
310
|
+
return parsed;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export { FractalPop, generate, parse, render, tokenize };
|
package/dist/full.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.js';
|