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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/core.d.ts +110 -0
  4. package/dist/core.js +313 -0
  5. package/dist/full.d.ts +1 -0
  6. package/dist/full.js +356 -0
  7. package/dist/gpu.d.ts +59 -0
  8. package/dist/gpu.js +215 -0
  9. package/dist/index.d.ts +108 -0
  10. package/dist/index.js +523 -0
  11. package/dist/lang/c.d.ts +5 -0
  12. package/dist/lang/c.js +90 -0
  13. package/dist/lang/cpp.d.ts +5 -0
  14. package/dist/lang/cpp.js +138 -0
  15. package/dist/lang/csharp.d.ts +5 -0
  16. package/dist/lang/csharp.js +148 -0
  17. package/dist/lang/css.d.ts +21 -0
  18. package/dist/lang/css.js +164 -0
  19. package/dist/lang/diff.d.ts +30 -0
  20. package/dist/lang/diff.js +18 -0
  21. package/dist/lang/dockerfile.d.ts +5 -0
  22. package/dist/lang/dockerfile.js +60 -0
  23. package/dist/lang/go.d.ts +5 -0
  24. package/dist/lang/go.js +91 -0
  25. package/dist/lang/graphql.d.ts +5 -0
  26. package/dist/lang/graphql.js +66 -0
  27. package/dist/lang/hcl.d.ts +5 -0
  28. package/dist/lang/hcl.js +51 -0
  29. package/dist/lang/html.d.ts +12 -0
  30. package/dist/lang/html.js +168 -0
  31. package/dist/lang/java.d.ts +5 -0
  32. package/dist/lang/java.js +96 -0
  33. package/dist/lang/javascript.d.ts +5 -0
  34. package/dist/lang/javascript.js +0 -0
  35. package/dist/lang/json.d.ts +5 -0
  36. package/dist/lang/json.js +47 -0
  37. package/dist/lang/kotlin.d.ts +5 -0
  38. package/dist/lang/kotlin.js +113 -0
  39. package/dist/lang/lua.d.ts +5 -0
  40. package/dist/lang/lua.js +65 -0
  41. package/dist/lang/markdown.d.ts +12 -0
  42. package/dist/lang/markdown.js +172 -0
  43. package/dist/lang/php.d.ts +5 -0
  44. package/dist/lang/php.js +129 -0
  45. package/dist/lang/plaintext.d.ts +7 -0
  46. package/dist/lang/plaintext.js +3 -0
  47. package/dist/lang/powershell.d.ts +5 -0
  48. package/dist/lang/powershell.js +74 -0
  49. package/dist/lang/python.d.ts +5 -0
  50. package/dist/lang/python.js +75 -0
  51. package/dist/lang/ruby.d.ts +5 -0
  52. package/dist/lang/ruby.js +84 -0
  53. package/dist/lang/rust.d.ts +5 -0
  54. package/dist/lang/rust.js +95 -0
  55. package/dist/lang/sass.d.ts +15 -0
  56. package/dist/lang/sass.js +201 -0
  57. package/dist/lang/scss.d.ts +14 -0
  58. package/dist/lang/scss.js +105 -0
  59. package/dist/lang/shell.d.ts +5 -0
  60. package/dist/lang/shell.js +64 -0
  61. package/dist/lang/sql.d.ts +5 -0
  62. package/dist/lang/sql.js +129 -0
  63. package/dist/lang/svelte.d.ts +15 -0
  64. package/dist/lang/svelte.js +0 -0
  65. package/dist/lang/swift.d.ts +5 -0
  66. package/dist/lang/swift.js +125 -0
  67. package/dist/lang/toml.d.ts +5 -0
  68. package/dist/lang/toml.js +45 -0
  69. package/dist/lang/typescript.d.ts +5 -0
  70. package/dist/lang/typescript.js +0 -0
  71. package/dist/lang/yaml.d.ts +5 -0
  72. package/dist/lang/yaml.js +64 -0
  73. package/dist/lang/zig.d.ts +5 -0
  74. package/dist/lang/zig.js +133 -0
  75. package/dist/lang.d.ts +30 -0
  76. package/dist/lang.js +345 -0
  77. 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
+ '&': '&amp;',
189
+ '<': '&lt;',
190
+ '>': '&gt;',
191
+ '"': '&quot;',
192
+ "'": '&#039;'
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';