elixir-data-viewer 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/README.md ADDED
@@ -0,0 +1,418 @@
1
+ # Elixir Data Viewer
2
+
3
+ A read-only web viewer for Elixir data structures with syntax highlighting, code folding, line numbers, and a VS Code Dark+ theme.
4
+
5
+ Built with vanilla TypeScript + DOM — no CodeMirror, no React — powered by [`lezer-elixir`](https://github.com/livebook-dev/lezer-elixir) for accurate parsing.
6
+
7
+ ## Features
8
+
9
+ - **Syntax Highlighting** — Accurate Elixir syntax coloring via `lezer-elixir` parser, matching VS Code Dark+ theme
10
+ - **Code Folding** — Collapse/expand maps, lists, tuples, keyword lists, bitstrings, and multi-line strings
11
+ - **Line Numbers** — Gutter with line numbers and fold indicators
12
+ - **Floating Toolbar** — Per-viewer toolbar (appears on hover) with:
13
+ - ⊟ Fold All
14
+ - ⊞ Unfold All
15
+ - ↩ Word Wrap toggle
16
+ - ⎘ Copy to clipboard
17
+ - ⧩ Filter by key
18
+ - **Key Filtering** — Hide specific key-value pairs by key name (e.g. filter out `socket`, `secret_key_base`)
19
+ - **Multiple Viewers** — Support multiple independent viewer instances on the same page
20
+ - **Configurable Toolbar** — Show/hide individual toolbar buttons via options or HTML `data-*` attributes
21
+ - **Word Wrap** — Toggle word wrap for long lines
22
+ - **Zero Dependencies** — Only peer dependencies on `lezer-elixir` and `@lezer/common`/`@lezer/highlight`
23
+
24
+ ## Supported Elixir Types
25
+
26
+ Maps (`%{}`), Lists (`[]`), Tuples (`{}`), Keyword Lists, Atoms (`:ok`), Strings (`"..."`), Integers, Floats, Booleans, `nil`, Charlists, Bitstrings (`<<>>`), Sigils, Heredoc strings, Character literals (`?A`)
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install elixir-data-viewer
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### As an npm Package
37
+
38
+ ```typescript
39
+ import { ElixirDataViewer } from "elixir-data-viewer";
40
+ import "elixir-data-viewer/style.css";
41
+
42
+ const container = document.getElementById("viewer")!;
43
+ const viewer = new ElixirDataViewer(container);
44
+ viewer.setContent(`%{name: "Alice", age: 30, roles: [:admin, :user]}`);
45
+ ```
46
+
47
+ ### With HTML Inline Data
48
+
49
+ Add `.edv-viewer` elements with `<script type="text/elixir-data">` blocks:
50
+
51
+ ```html
52
+ <div class="edv-viewer">
53
+ <script type="text/elixir-data">
54
+ %{
55
+ name: "Alice",
56
+ age: 30,
57
+ roles: [:admin, :user]
58
+ }
59
+ </script>
60
+ </div>
61
+
62
+ <script type="module">
63
+ import { ElixirDataViewer } from "elixir-data-viewer";
64
+ import "elixir-data-viewer/style.css";
65
+
66
+ document.querySelectorAll(".edv-viewer").forEach((el) => {
67
+ const script = el.querySelector('script[type="text/elixir-data"]');
68
+ if (!script) return;
69
+ const data = script.textContent?.trim() ?? "";
70
+ script.remove();
71
+ const viewer = new ElixirDataViewer(el);
72
+ viewer.setContent(data);
73
+ });
74
+ </script>
75
+ ```
76
+
77
+ ### Standalone / Phoenix (Single ESM File)
78
+
79
+ Build a single JS file with all dependencies and CSS bundled in:
80
+
81
+ ```bash
82
+ npm run build:standalone
83
+ ```
84
+
85
+ This produces `dist/elixir-data-viewer.js` (~55 kB gzipped) — a single ESM file that:
86
+ - Bundles all dependencies (`lezer-elixir`, `@lezer/common`, `@lezer/highlight`)
87
+ - Injects CSS automatically via `<style>` tag (no separate CSS file needed)
88
+ - Exports `ElixirDataViewer` as the default export, plus all named exports
89
+
90
+ #### Phoenix Integration
91
+
92
+ 1. Copy the built file into your Phoenix project:
93
+
94
+ ```bash
95
+ cp dist/elixir-data-viewer.js your_phoenix_app/assets/vendor/
96
+ ```
97
+
98
+ 2. Import it in your `assets/js/app.js`:
99
+
100
+ ```javascript
101
+ import ElixirDataViewer from "../vendor/elixir-data-viewer"
102
+ ```
103
+
104
+ 3. Create a LiveView Hook:
105
+
106
+ ```javascript
107
+ let Hooks = {};
108
+
109
+ Hooks.ElixirDataViewer = {
110
+ mounted() {
111
+ const viewer = new ElixirDataViewer(this.el);
112
+ viewer.setContent(this.el.dataset.content || this.el.innerText);
113
+ this.viewer = viewer;
114
+
115
+ // Optional: handle LiveView updates
116
+ this.handleEvent("update-viewer", ({ content }) => {
117
+ this.viewer.setContent(content);
118
+ });
119
+ },
120
+ updated() {
121
+ this.viewer.setContent(this.el.dataset.content || this.el.innerText);
122
+ },
123
+ };
124
+
125
+ // Pass hooks to LiveSocket
126
+ let liveSocket = new LiveSocket("/live", Socket, {
127
+ hooks: Hooks,
128
+ // ...
129
+ });
130
+ ```
131
+
132
+ 4. Use in your LiveView template:
133
+
134
+ ```heex
135
+ <div id="data-viewer" phx-hook="ElixirDataViewer" data-content={inspect(@data, pretty: true)}>
136
+ </div>
137
+ ```
138
+
139
+ ## API Reference
140
+
141
+ ### `ElixirDataViewer`
142
+
143
+ The main viewer class.
144
+
145
+ ```typescript
146
+ const viewer = new ElixirDataViewer(container: HTMLElement, options?: ElixirDataViewerOptions);
147
+ ```
148
+
149
+ #### Constructor Options
150
+
151
+ ```typescript
152
+ interface ElixirDataViewerOptions {
153
+ toolbar?: {
154
+ foldAll?: boolean; // Show "Fold All" button (default: true)
155
+ unfoldAll?: boolean; // Show "Unfold All" button (default: true)
156
+ wordWrap?: boolean; // Show "Word Wrap" toggle (default: true)
157
+ copy?: boolean; // Show "Copy" button (default: true)
158
+ search?: boolean; // Show "Search" button (default: true)
159
+ filter?: boolean; // Show "Filter" button (default: true)
160
+ };
161
+ /** Default fold level — regions deeper than this are auto-folded on setContent().
162
+ * E.g. 3 = show first 3 levels expanded, fold level 4+.
163
+ * 0 or undefined = no auto-folding (all expanded). */
164
+ defaultFoldLevel?: number;
165
+ /** Whether word wrap is enabled by default. Default: false */
166
+ defaultWordWrap?: boolean;
167
+ /** Keys to filter out by default when setContent() is called */
168
+ defaultFilterKeys?: string[];
169
+ }
170
+ ```
171
+
172
+ **Examples:**
173
+
174
+ ```typescript
175
+ // All toolbar buttons visible (default)
176
+ new ElixirDataViewer(container);
177
+
178
+ // Hide the copy button
179
+ new ElixirDataViewer(container, { toolbar: { copy: false } });
180
+
181
+ // No toolbar at all
182
+ new ElixirDataViewer(container, {
183
+ toolbar: { foldAll: false, unfoldAll: false, wordWrap: false, copy: false, search: false, filter: false }
184
+ });
185
+
186
+ // Auto-fold: show only top 3 levels, fold level 4+
187
+ new ElixirDataViewer(container, { defaultFoldLevel: 3 });
188
+
189
+ // Enable word wrap by default
190
+ new ElixirDataViewer(container, { defaultWordWrap: true });
191
+
192
+ // Filter out specific keys by default
193
+ new ElixirDataViewer(container, { defaultFilterKeys: ["socket", "secret_key_base"] });
194
+ ```
195
+
196
+ #### HTML Data Attributes
197
+
198
+ When using auto-discovery, toolbar buttons and fold level can be configured via `data-*` attributes:
199
+
200
+ ```html
201
+ <!-- Hide copy and fold-all buttons -->
202
+ <div class="edv-viewer" data-toolbar-copy="false" data-toolbar-fold-all="false">
203
+ <script type="text/elixir-data">...</script>
204
+ </div>
205
+
206
+ <!-- Auto-fold: show only top 3 levels -->
207
+ <div class="edv-viewer" data-fold-level="3">
208
+ <script type="text/elixir-data">...</script>
209
+ </div>
210
+
211
+ <!-- Filter out specific keys by default -->
212
+ <div class="edv-viewer" data-filter-keys="socket,secret_key_base">
213
+ <script type="text/elixir-data">...</script>
214
+ </div>
215
+ ```
216
+
217
+ Available attributes: `data-toolbar-fold-all`, `data-toolbar-unfold-all`, `data-toolbar-word-wrap`, `data-toolbar-copy`, `data-toolbar-search`, `data-toolbar-filter`, `data-fold-level`, `data-filter-keys`
218
+
219
+ #### Methods
220
+
221
+ | Method | Description |
222
+ |--------|-------------|
223
+ | `setContent(code: string): void` | Set the Elixir data content and render |
224
+ | `getContent(): string` | Get the raw Elixir data string |
225
+ | `foldAll(): void` | Collapse all foldable regions |
226
+ | `unfoldAll(): void` | Expand all folded regions |
227
+ | `foldToLevel(level: number): void` | Fold regions deeper than `level` (1 = top-level). `foldToLevel(3)` shows levels 1–3, folds 4+. `foldToLevel(0)` unfolds all. |
228
+ | `toggleWordWrap(): void` | Toggle word wrap mode |
229
+ | `isWordWrap(): boolean` | Get current word wrap state |
230
+ | `copyContent(): Promise<void>` | Copy raw content to clipboard |
231
+ | `onRender(callback: () => void): void` | Set a callback after each render |
232
+ | `onInspect(callback: ((event: InspectEvent) => void) \| null): void` | Set a callback when a value is clicked (see below) |
233
+ | `setFilterKeys(keys: string[]): void` | Set keys to filter out (replaces existing). Re-renders. |
234
+ | `addFilterKey(key: string): void` | Add a single key to filter. Re-renders. |
235
+ | `removeFilterKey(key: string): void` | Remove a single key from filter. Re-renders. |
236
+ | `getFilterKeys(): string[]` | Get currently filtered key names |
237
+ | `getAvailableKeys(): string[]` | Get all key names detected in current content |
238
+ | `clearFilter(): void` | Remove all key filters. Re-renders. |
239
+ | `openFilter(): void` | Open the filter bar UI |
240
+ | `closeFilter(): void` | Close the filter bar UI |
241
+ | `toggleFilter(): void` | Toggle filter bar visibility |
242
+
243
+ #### `onInspect` — Custom Click Handling
244
+
245
+ Register a callback that fires when the user clicks an inspectable value (string, atom, number, structure, etc.). The callback receives an `InspectEvent` with the value's type, text, DOM element reference, and a `preventDefault()` method to suppress the default copy-to-clipboard behavior.
246
+
247
+ ```typescript
248
+ interface InspectEvent {
249
+ type: InspectType; // "String" | "Atom" | "Integer" | "Map" | ...
250
+ copyText: string; // The text that would be copied
251
+ target: InspectTarget; // Full target with from/to offsets
252
+ element: HTMLElement; // The clicked DOM element
253
+ mouseEvent: MouseEvent; // The original click event
254
+ preventDefault(): void; // Call to suppress default copy + toast
255
+ }
256
+ ```
257
+
258
+ **Example: Log to console, suppress copy for strings**
259
+
260
+ ```typescript
261
+ viewer.onInspect((event) => {
262
+ console.log(`Clicked ${event.type}: ${event.copyText}`);
263
+
264
+ if (event.type === "String") {
265
+ event.preventDefault();
266
+ // Custom handling — e.g. open a modal
267
+ }
268
+ // Other types still get the default copy behavior
269
+ });
270
+ ```
271
+
272
+ **Example: Render string content as Markdown in a modal**
273
+
274
+ ```typescript
275
+ viewer.onInspect((event) => {
276
+ if (event.type === "String") {
277
+ event.preventDefault();
278
+ const content = event.copyText.slice(1, -1); // strip quotes
279
+ showMarkdownModal(content, event.element);
280
+ }
281
+ });
282
+ ```
283
+
284
+ **Unregister the callback:**
285
+
286
+ ```typescript
287
+ viewer.onInspect(null); // Restore default copy behavior
288
+ ```
289
+
290
+ #### Key Filtering — Hide Keys by Name
291
+
292
+ Filter out specific key-value pairs from the rendered view. The data is not modified — only the visual rendering skips the lines belonging to filtered keys.
293
+
294
+ **Example: Programmatic filtering via API**
295
+
296
+ ```typescript
297
+ const viewer = new ElixirDataViewer(container);
298
+ viewer.setContent(`%{
299
+ name: "Alice",
300
+ socket: #Port<0.80>,
301
+ secret: "s3cr3t"
302
+ }`);
303
+
304
+ // Hide "socket" and "secret" keys
305
+ viewer.setFilterKeys(["socket", "secret"]);
306
+
307
+ // Add one more key to hide
308
+ viewer.addFilterKey("pid");
309
+
310
+ // Remove a key from the filter
311
+ viewer.removeFilterKey("secret");
312
+
313
+ // Get all keys detected in the content
314
+ console.log(viewer.getAvailableKeys());
315
+ // → ["name", "secret", "socket"]
316
+
317
+ // Clear all filters
318
+ viewer.clearFilter();
319
+ ```
320
+
321
+ **Example: Default filter keys via options**
322
+
323
+ ```typescript
324
+ new ElixirDataViewer(container, {
325
+ defaultFilterKeys: ["socket", "secret_key_base"]
326
+ });
327
+ ```
328
+
329
+ **Example: Filter via HTML data attribute**
330
+
331
+ ```html
332
+ <div class="edv-viewer" data-filter-keys="socket,secret_key_base">
333
+ <script type="text/elixir-data">...</script>
334
+ </div>
335
+ ```
336
+
337
+ ### Lower-Level Exports
338
+
339
+ For advanced use, the parser, highlighter, fold, and filter modules are also exported:
340
+
341
+ ```typescript
342
+ import {
343
+ parseElixir, // Parse Elixir code → Lezer Tree
344
+ highlight, // Tree → HighlightToken[]
345
+ getLineTokens, // Filter tokens for a specific line
346
+ detectFoldRegions, // Tree → FoldRegion[]
347
+ buildFoldMap, // FoldRegion[] → Map<number, FoldRegion>
348
+ FoldState, // Fold state manager
349
+ FilterState, // Filter state manager
350
+ } from "elixir-data-viewer";
351
+ ```
352
+
353
+ ## Theming
354
+
355
+ The viewer uses CSS classes for all styling. Import the default VS Code Dark+ theme:
356
+
357
+ ```typescript
358
+ import "elixir-data-viewer/style.css";
359
+ ```
360
+
361
+ Key CSS classes you can override:
362
+
363
+ | Class | Description |
364
+ |-------|-------------|
365
+ | `.edv-container` | Main viewer container |
366
+ | `.edv-line` | A single line row |
367
+ | `.edv-gutter` | Line number + fold gutter |
368
+ | `.edv-code` | Code content area |
369
+ | `.edv-toolbar` | Floating toolbar |
370
+ | `.edv-toolbar-btn` | Toolbar button |
371
+ | `.tok-atom`, `.tok-string`, etc. | Syntax token colors |
372
+
373
+ ## Development
374
+
375
+ ```bash
376
+ # Install dependencies
377
+ npm install
378
+
379
+ # Start dev server with hot reload
380
+ npm run dev
381
+
382
+ # Build for production (library: ES + CJS)
383
+ npm run build
384
+
385
+ # Build standalone IIFE (single file with all deps + CSS)
386
+ npm run build:standalone
387
+
388
+ # Preview production build
389
+ npm run preview
390
+ ```
391
+
392
+ The dev server starts at `http://localhost:5173` with the demo page showing multiple viewer instances.
393
+
394
+ ## Project Structure
395
+
396
+ ```
397
+ ├── index.html # Demo page with multiple viewers
398
+ ├── src/
399
+ │ ├── main.ts # Demo entry — auto-discovers .edv-viewer elements
400
+ │ ├── index.ts # Library entry — public exports
401
+ │ ├── standalone.ts # Standalone entry — default export for Phoenix/vendor
402
+ │ ├── renderer.ts # ElixirDataViewer class with toolbar
403
+ │ ├── parser.ts # lezer-elixir parser wrapper
404
+ │ ├── highlighter.ts # Syntax tree → highlighted tokens
405
+ │ ├── fold.ts # Fold region detection
406
+ │ ├── state.ts # Fold state management
407
+ │ └── styles/
408
+ │ └── theme.css # Tokyo Night theme
409
+ ├── package.json
410
+ ├── tsconfig.json
411
+ ├── tsconfig.build.json
412
+ ├── vite.config.ts # Library build config (ES + CJS)
413
+ └── vite.config.standalone.ts # Standalone ESM build config (single file)
414
+ ```
415
+
416
+ ## License
417
+
418
+ MIT
@@ -0,0 +1,78 @@
1
+ import type { Tree } from "@lezer/common";
2
+ /**
3
+ * Represents a key-value pair's line range in the source.
4
+ */
5
+ export interface KeyRange {
6
+ /** The key name (e.g. "socket", "name") */
7
+ key: string;
8
+ /** 0-indexed first line of the key-value pair */
9
+ startLine: number;
10
+ /** 0-indexed last line of the key-value pair (inclusive) */
11
+ endLine: number;
12
+ /** Nesting depth (1 = direct child of root structure) */
13
+ depth: number;
14
+ }
15
+ /**
16
+ * Manages the filter state for the viewer — which keys are hidden.
17
+ *
18
+ * Walks the Lezer syntax tree to detect all key-value pair ranges,
19
+ * then hides lines belonging to filtered keys during rendering.
20
+ */
21
+ export declare class FilterState {
22
+ /** Keys to filter out (hide) */
23
+ private filteredKeys;
24
+ /** All detected key-value ranges from the parsed content */
25
+ private keyRanges;
26
+ /** Pre-computed set of hidden line indices (rebuilt when filter changes) */
27
+ private hiddenLines;
28
+ /**
29
+ * Detect all key-value ranges from the syntax tree and source code.
30
+ * Called when content changes via setContent().
31
+ */
32
+ detectKeys(tree: Tree, code: string): void;
33
+ /**
34
+ * Set the keys to filter out (replaces existing filter).
35
+ */
36
+ setKeys(keys: string[]): void;
37
+ /**
38
+ * Add a single key to the filter.
39
+ */
40
+ addKey(key: string): void;
41
+ /**
42
+ * Remove a single key from the filter.
43
+ */
44
+ removeKey(key: string): void;
45
+ /**
46
+ * Check if a key is currently being filtered.
47
+ */
48
+ hasKey(key: string): boolean;
49
+ /**
50
+ * Get all currently filtered keys.
51
+ */
52
+ getKeys(): string[];
53
+ /**
54
+ * Get all available keys detected in the content.
55
+ * Returns unique key names sorted alphabetically.
56
+ */
57
+ getAvailableKeys(): string[];
58
+ /**
59
+ * Clear all filters (show all keys).
60
+ */
61
+ clear(): void;
62
+ /**
63
+ * Check if a specific line should be hidden due to filtering.
64
+ */
65
+ isLineFiltered(lineIdx: number): boolean;
66
+ /**
67
+ * Check if any filter is active.
68
+ */
69
+ isActive(): boolean;
70
+ /**
71
+ * Get the total number of filtered keys.
72
+ */
73
+ getFilteredCount(): number;
74
+ /**
75
+ * Rebuild the set of hidden line indices based on current filtered keys.
76
+ */
77
+ private rebuildHiddenLines;
78
+ }
package/dist/fold.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { Tree } from "@lezer/common";
2
+ /**
3
+ * Represents a foldable region in the code.
4
+ */
5
+ export interface FoldRegion {
6
+ /** 0-indexed line where the fold starts */
7
+ startLine: number;
8
+ /** 0-indexed line where the fold ends */
9
+ endLine: number;
10
+ /** Character offset in the source where the opening bracket is */
11
+ startOffset: number;
12
+ /** Character offset in the source where the closing bracket is */
13
+ endOffset: number;
14
+ /** The opening bracket text, e.g. "[", "%{", "{" */
15
+ openText: string;
16
+ /** The closing bracket text, e.g. "]", "}", ">>" */
17
+ closeText: string;
18
+ /** Number of direct child items in the structure (e.g. list elements, map pairs). -1 if not applicable. */
19
+ itemCount: number;
20
+ /** Nesting depth of this foldable region (1 = top-level, 2 = nested inside one, etc.) */
21
+ depth: number;
22
+ }
23
+ /**
24
+ * Detect all foldable regions in the given Elixir code.
25
+ */
26
+ export declare function detectFoldRegions(code: string, tree: Tree): FoldRegion[];
27
+ /**
28
+ * Build a map from startLine to its FoldRegion for quick lookup.
29
+ */
30
+ export declare function buildFoldMap(regions: FoldRegion[]): Map<number, FoldRegion>;
@@ -0,0 +1,19 @@
1
+ import type { Tree } from "@lezer/common";
2
+ /**
3
+ * A highlighted token span with positional info and CSS classes.
4
+ */
5
+ export interface HighlightToken {
6
+ from: number;
7
+ to: number;
8
+ classes: string;
9
+ }
10
+ /**
11
+ * Walk the Lezer syntax tree and emit highlight tokens using
12
+ * a custom tagHighlighter that maps Elixir tags to tok-* CSS classes.
13
+ */
14
+ export declare function highlight(code: string, tree: Tree): HighlightToken[];
15
+ /**
16
+ * Given a line's start/end offsets within the full code, extract the
17
+ * tokens that overlap this line and adjust their offsets to be line-relative.
18
+ */
19
+ export declare function getLineTokens(tokens: HighlightToken[], lineStart: number, lineEnd: number): HighlightToken[];
package/dist/index.cjs ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";var $=Object.defineProperty;var q=(r,t,e)=>t in r?$(r,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[t]=e;var a=(r,t,e)=>q(r,typeof t!="symbol"?t+"":t,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const V=require("lezer-elixir"),p=require("@lezer/highlight");function k(r){return V.parser.parse(r)}const _=p.tagHighlighter([{tag:p.tags.atom,class:"tok-atom"},{tag:p.tags.namespace,class:"tok-namespace"},{tag:p.tags.bool,class:"tok-bool"},{tag:p.tags.null,class:"tok-null"},{tag:p.tags.integer,class:"tok-number"},{tag:p.tags.float,class:"tok-number"},{tag:p.tags.character,class:"tok-character"},{tag:p.tags.variableName,class:"tok-variableName"},{tag:p.tags.function(p.tags.variableName),class:"tok-function"},{tag:p.tags.definition(p.tags.function(p.tags.variableName)),class:"tok-definition"},{tag:p.tags.special(p.tags.variableName),class:"tok-special"},{tag:p.tags.string,class:"tok-string"},{tag:p.tags.special(p.tags.string),class:"tok-string"},{tag:p.tags.escape,class:"tok-escape"},{tag:p.tags.keyword,class:"tok-keyword"},{tag:p.tags.operator,class:"tok-operator"},{tag:p.tags.lineComment,class:"tok-comment"},{tag:p.tags.comment,class:"tok-underscore"},{tag:p.tags.paren,class:"tok-punctuation"},{tag:p.tags.squareBracket,class:"tok-punctuation"},{tag:p.tags.brace,class:"tok-punctuation"},{tag:p.tags.special(p.tags.brace),class:"tok-punctuation"},{tag:p.tags.separator,class:"tok-separator"},{tag:p.tags.angleBracket,class:"tok-angleBracket"},{tag:p.tags.attributeName,class:"tok-attributeName"},{tag:p.tags.docString,class:"tok-docString"}]);function x(r,t){const e=[];return p.highlightTree(t,_,(s,i,n)=>{e.push({from:s,to:i,classes:n})}),e}function C(r,t,e){const s=[];for(const i of r)i.to<=t||i.from>=e||s.push({from:Math.max(i.from,t)-t,to:Math.min(i.to,e)-t,classes:i.classes});return s}const U={List:{open:"[",close:"]"},Tuple:{open:"{",close:"}"},Map:{open:"%{",close:"}"},Bitstring:{open:"<<",close:">>"},AnonymousFunction:{open:"fn",close:"end"},String:{open:'"""',close:'"""'},Charlist:{open:"'''",close:"'''"}},z=new Set(["List","Tuple","Map","MapContent","Keywords","Bitstring"]),Q=new Set([",","[","]","{","}","<<",">>","%","|","=>",":"]);function B(r){const t=r.type.name;if(!z.has(t))return-1;let e=0,s=r.firstChild;for(;s;){const i=s.type.name;if(Q.has(i)){s=s.nextSibling;continue}if(i==="MapContent"||i==="Keywords")return B(s);if(i==="Struct"){s=s.nextSibling;continue}e++,s=s.nextSibling}return e}function Y(r){const t=[0];for(let e=0;e<r.length;e++)r[e]===`
2
+ `&&t.push(e+1);return t}function S(r,t){let e=0,s=r.length-1;for(;e<s;){const i=e+s+1>>1;r[i]<=t?e=i:s=i-1}return e}function T(r,t,e,s){const i=U[r.type.name];if(i){const l=S(e,r.from),o=S(e,r.to-1);if(o>l){let c=i.open,d=r.from;if(r.type.name==="String"||r.type.name==="Charlist"){const g=t.slice(r.from,Math.min(r.from+3,r.to));g==='"""'||g==="'''"?c=g:c=g[0]||i.open}s.push({startLine:l,endLine:o,startOffset:d,endOffset:r.to,openText:c,closeText:i.close,itemCount:B(r),depth:0})}}let n=r.firstChild;for(;n;)T(n,t,e,s),n=n.nextSibling}function F(r,t){const e=Y(r),s=[];T(t.topNode,r,e,s),s.sort((n,l)=>n.startLine-l.startLine||n.startOffset-l.startOffset);const i=[];for(const n of s){for(;i.length>0&&i[i.length-1].endOffset<=n.startOffset;)i.pop();n.depth=i.length+1,i.push(n)}return s}function A(r){const t=new Map;for(const e of r)t.has(e.startLine)||t.set(e.startLine,e);return t}class K{constructor(){a(this,"foldedLines",new Set);a(this,"regionMap",new Map);a(this,"regions",[])}setRegions(t,e){this.regions=t,this.regionMap=e,this.foldedLines.clear()}toggle(t){this.foldedLines.has(t)?this.foldedLines.delete(t):this.regionMap.has(t)&&this.foldedLines.add(t)}isFolded(t){return this.foldedLines.has(t)}getRegion(t){return this.regionMap.get(t)}isLineHidden(t){for(const e of this.foldedLines){const s=this.regionMap.get(e);if(s&&t>s.startLine&&t<=s.endLine)return s}}isFoldable(t){return this.regionMap.has(t)}getRegions(){return this.regions}foldAll(){for(const t of this.regions)this.foldedLines.add(t.startLine)}unfoldAll(){this.foldedLines.clear()}foldToLevel(t){if(this.foldedLines.clear(),!(t<=0))for(const e of this.regions)e.depth>t&&this.foldedLines.add(e.startLine)}revealLine(t){for(const e of this.foldedLines){const s=this.regionMap.get(e);s&&t>s.startLine&&t<=s.endLine&&this.foldedLines.delete(e)}}}class M{constructor(){a(this,"query","");a(this,"caseSensitive",!1);a(this,"matches",[]);a(this,"currentIndex",-1)}search(t,e,s){if(this.query=e,this.caseSensitive=s,this.matches=[],this.currentIndex=-1,!e)return!0;const i=s?e:e.toLowerCase();for(let n=0;n<t.length;n++){const l=s?t[n]:t[n].toLowerCase();let o=0;for(;o<=l.length-i.length;){const c=l.indexOf(i,o);if(c===-1)break;this.matches.push({line:n,from:c,to:c+i.length}),o=c+1}}return this.matches.length>0&&(this.currentIndex=0),!0}clear(){this.query="",this.matches=[],this.currentIndex=-1}getQuery(){return this.query}isCaseSensitive(){return this.caseSensitive}getMatches(){return this.matches}getLineMatches(t){return this.matches.filter(e=>e.line===t)}getCurrentIndex(){return this.currentIndex}getCurrentMatch(){if(!(this.currentIndex<0||this.currentIndex>=this.matches.length))return this.matches[this.currentIndex]}getMatchCount(){return this.matches.length}next(){if(this.matches.length!==0)return this.currentIndex=(this.currentIndex+1)%this.matches.length,this.matches[this.currentIndex]}prev(){if(this.matches.length!==0)return this.currentIndex=(this.currentIndex-1+this.matches.length)%this.matches.length,this.matches[this.currentIndex]}setCurrentToLine(t){if(this.matches.length!==0){for(let e=0;e<this.matches.length;e++)if(this.matches[e].line>=t){this.currentIndex=e;return}this.currentIndex=0}}isCurrentMatch(t,e){const s=this.getCurrentMatch();return s?s.line===t&&s.from===e:!1}isActive(){return this.query.length>0}}class D{constructor(){a(this,"filteredKeys",new Set);a(this,"keyRanges",[]);a(this,"hiddenLines",new Set)}detectKeys(t,e){const s=j(e);this.keyRanges=[],R(t.topNode,e,s,this.keyRanges,0),this.rebuildHiddenLines()}setKeys(t){this.filteredKeys=new Set(t),this.rebuildHiddenLines()}addKey(t){this.filteredKeys.add(t),this.rebuildHiddenLines()}removeKey(t){this.filteredKeys.delete(t),this.rebuildHiddenLines()}hasKey(t){return this.filteredKeys.has(t)}getKeys(){return Array.from(this.filteredKeys)}getAvailableKeys(){const t=new Set;for(const e of this.keyRanges)t.add(e.key);return Array.from(t).sort()}clear(){this.filteredKeys.clear(),this.hiddenLines.clear()}isLineFiltered(t){return this.hiddenLines.has(t)}isActive(){return this.filteredKeys.size>0}getFilteredCount(){return this.filteredKeys.size}rebuildHiddenLines(){if(this.hiddenLines.clear(),this.filteredKeys.size!==0){for(const t of this.keyRanges)if(this.filteredKeys.has(t.key))for(let e=t.startLine;e<=t.endLine;e++)this.hiddenLines.add(e)}}}function j(r){const t=[0];for(let e=0;e<r.length;e++)r[e]===`
3
+ `&&t.push(e+1);return t}function L(r,t){let e=0,s=r.length-1;for(;e<s;){const i=e+s+1>>1;r[i]<=t?e=i:s=i-1}return e}function R(r,t,e,s,i){const n=r.type.name;if(n==="Pair"){const d=Z(r,t);if(d!==null){const g=L(e,r.from),f=L(e,r.to-1);s.push({key:d,startLine:g,endLine:f,depth:i})}}const o=n==="Map"||n==="List"||n==="Tuple"||n==="MapContent"||n==="Keywords"?i+1:i;let c=r.firstChild;for(;c;)R(c,t,e,s,o),c=c.nextSibling}function Z(r,t){const e=r.firstChild;if(!e)return null;const s=e.type.name,i=t.slice(e.from,e.to);return s==="Keyword"?i.replace(/:\s*$/,""):s==="Atom"?i.replace(/^:/,""):s==="String"?i.replace(/^"/,"").replace(/"$/,""):i.length>0?i:null}const b=new Set(["Map","List","Tuple","Bitstring"]),X=new Set(["{","}","[","]","<<",">>"]),I=new Set(["String","Atom","Alias","Integer","Float","Boolean","Nil","Char","Charlist","Sigil"]),G=new Set(["QuotedContent"]);function N(r,t,e){if(e<0||e>=t.length)return null;const s=r.resolveInner(e,1);return s?J(s,t):null}function J(r,t){const e=r.type.name;if(X.has(e)){const i=r.parent;if(i&&b.has(i.type.name))return{from:i.from,to:i.to,copyText:t.slice(i.from,i.to),isStructure:!0,type:i.type.name}}if(b.has(e))return{from:r.from,to:r.to,copyText:t.slice(r.from,r.to),isStructure:!0,type:e};const s=tt(r,t);if(s)return s;if(G.has(e)){const i=r.parent;if(i&&I.has(i.type.name))return{from:i.from,to:i.to,copyText:t.slice(i.from,i.to),isStructure:!1,type:i.type.name}}if(I.has(e))return{from:r.from,to:r.to,copyText:t.slice(r.from,r.to),isStructure:!1,type:e};if(e==="Keyword"){const n=":"+t.slice(r.from,r.to).replace(/:\s*$/,"");return{from:r.from,to:r.to,copyText:n,isStructure:!1,type:"Keyword"}}return e==="Pair"?{from:r.from,to:r.to,copyText:t.slice(r.from,r.to),isStructure:!1,type:"Pair"}:null}function O(r,t){if(r.type.name!=="BinaryOperator")return!1;let e=r.firstChild;for(;e;){if(e.type.name==="Operator"&&t.slice(e.from,e.to)==="..")return!0;e=e.nextSibling}return!1}function w(r,t){if(r.type.name!=="BinaryOperator")return!1;let e=!1,s=!1,i=r.firstChild;for(;i;)i.type.name==="Operator"&&t.slice(i.from,i.to)==="//"&&(e=!0),O(i,t)&&(s=!0),i=i.nextSibling;return e&&s}function tt(r,t){let e=r;for(;e;){if(e.type.name==="BinaryOperator"){if(w(e,t))return{from:e.from,to:e.to,copyText:t.slice(e.from,e.to),isStructure:!1,type:"Range"};if(O(e,t))return e.parent&&w(e.parent,t)?{from:e.parent.from,to:e.parent.to,copyText:t.slice(e.parent.from,e.parent.to),isStructure:!1,type:"Range"}:{from:e.from,to:e.to,copyText:t.slice(e.from,e.to),isStructure:!1,type:"Range"}}e=e.parent}return null}const et=/#[A-Z][\w.]*<[^>\n]*>/g;function H(r){const t=[];return{modifiedCode:r.replace(et,(s,i)=>(t.push({from:i,to:i+s.length,originalText:s}),":"+"_".repeat(s.length-1))),inspectLiterals:t}}class st{constructor(t,e){a(this,"container");a(this,"innerEl");a(this,"scrollEl");a(this,"toolbarEl",null);a(this,"wrapBtn",null);a(this,"copyBtn",null);a(this,"searchBtn",null);a(this,"filterBtn",null);a(this,"copyResetTimer",null);a(this,"code","");a(this,"lines",[]);a(this,"lineOffsets",[]);a(this,"tokens",[]);a(this,"tree",null);a(this,"foldState",new K);a(this,"filterState",new D);a(this,"defaultFoldLevel",0);a(this,"defaultFilterKeys",[]);a(this,"searchState",new M);a(this,"onRenderCallback",null);a(this,"wordWrap",!1);a(this,"toolbarConfig");a(this,"searchBarEl",null);a(this,"searchInputEl",null);a(this,"searchInfoEl",null);a(this,"searchCaseBtn",null);a(this,"searchVisible",!1);a(this,"filterBarEl",null);a(this,"filterInputEl",null);a(this,"filterTagsEl",null);a(this,"filterInfoEl",null);a(this,"filterDropdownEl",null);a(this,"filterCopyBtn",null);a(this,"filterCopyResetTimer",null);a(this,"filterVisible",!1);a(this,"filterDropdownIndex",-1);a(this,"filterDropdownItems",[]);a(this,"currentInspect",null);a(this,"inspectCallback",null);a(this,"inspectLiterals",[]);this.container=t,this.container.classList.add("edv-container"),this.defaultFoldLevel=(e==null?void 0:e.defaultFoldLevel)??0,this.defaultFilterKeys=(e==null?void 0:e.defaultFilterKeys)??[],this.wordWrap=(e==null?void 0:e.defaultWordWrap)??!1;const s=(e==null?void 0:e.toolbar)??{};this.toolbarConfig={foldAll:s.foldAll!==!1,unfoldAll:s.unfoldAll!==!1,wordWrap:s.wordWrap!==!1,copy:s.copy!==!1,search:s.search!==!1,filter:s.filter!==!1},this.buildToolbar(),this.wordWrap&&(this.container.classList.add("edv-word-wrap"),this.wrapBtn&&this.wrapBtn.classList.add("edv-toolbar-btn--active")),this.buildSearchBar(),this.buildFilterBar(),this.innerEl=document.createElement("div"),this.innerEl.classList.add("edv-inner"),this.container.appendChild(this.innerEl),this.scrollEl=document.createElement("div"),this.scrollEl.classList.add("edv-scroll"),this.innerEl.appendChild(this.scrollEl),this.scrollEl.addEventListener("mouseover",i=>this.handleInspectHover(i)),this.scrollEl.addEventListener("mouseout",i=>this.handleInspectOut(i)),this.scrollEl.addEventListener("click",i=>this.handleInspectClick(i)),this.container.setAttribute("tabindex","0"),this.container.addEventListener("keydown",i=>this.handleKeyDown(i))}buildToolbar(){const t=this.toolbarConfig;if(t.foldAll||t.unfoldAll||t.wordWrap||t.copy||t.search||t.filter){if(this.toolbarEl=document.createElement("div"),this.toolbarEl.classList.add("edv-toolbar"),t.search&&(this.searchBtn=this.createToolbarButton("⌕","Search (Ctrl+F)",()=>this.toggleSearch()),this.toolbarEl.appendChild(this.searchBtn)),t.filter&&(this.filterBtn=this.createToolbarButton("⧩","Filter Keys",()=>this.toggleFilter()),this.toolbarEl.appendChild(this.filterBtn)),t.foldAll){const s=this.createToolbarButton("⊟","Fold All",()=>this.foldAll());this.toolbarEl.appendChild(s)}if(t.unfoldAll){const s=this.createToolbarButton("⊞","Unfold All",()=>this.unfoldAll());this.toolbarEl.appendChild(s)}t.wordWrap&&(this.wrapBtn=this.createToolbarButton("↩","Word Wrap (Alt+Z)",()=>{this.toggleWordWrap()}),this.toolbarEl.appendChild(this.wrapBtn)),t.copy&&(this.copyBtn=this.createToolbarButton("⎘","Copy",()=>this.copyContent()),this.toolbarEl.appendChild(this.copyBtn)),this.container.appendChild(this.toolbarEl)}}buildSearchBar(){this.searchBarEl=document.createElement("div"),this.searchBarEl.classList.add("edv-search-bar");const t=document.createElement("div");t.classList.add("edv-search-input-wrapper"),this.searchInputEl=document.createElement("input"),this.searchInputEl.type="text",this.searchInputEl.classList.add("edv-search-input"),this.searchInputEl.placeholder="Search…",this.searchInputEl.addEventListener("input",()=>this.onSearchInput()),this.searchInputEl.addEventListener("keydown",n=>this.handleSearchKeyDown(n)),this.searchCaseBtn=document.createElement("button"),this.searchCaseBtn.classList.add("edv-search-case-btn"),this.searchCaseBtn.textContent="Aa",this.searchCaseBtn.title="Match Case",this.searchCaseBtn.addEventListener("click",n=>{n.stopPropagation(),this.toggleCaseSensitive()}),t.appendChild(this.searchInputEl),t.appendChild(this.searchCaseBtn),this.searchBarEl.appendChild(t),this.searchInfoEl=document.createElement("span"),this.searchInfoEl.classList.add("edv-search-info"),this.searchBarEl.appendChild(this.searchInfoEl);const e=document.createElement("button");e.classList.add("edv-search-nav-btn"),e.textContent="↑",e.title="Previous Match (Shift+Enter)",e.addEventListener("click",n=>{n.stopPropagation(),this.searchPrev()}),this.searchBarEl.appendChild(e);const s=document.createElement("button");s.classList.add("edv-search-nav-btn"),s.textContent="↓",s.title="Next Match (Enter)",s.addEventListener("click",n=>{n.stopPropagation(),this.searchNext()}),this.searchBarEl.appendChild(s);const i=document.createElement("button");i.classList.add("edv-search-nav-btn"),i.textContent="✕",i.title="Close (Escape)",i.addEventListener("click",n=>{n.stopPropagation(),this.closeSearch()}),this.searchBarEl.appendChild(i),this.container.appendChild(this.searchBarEl)}handleKeyDown(t){(t.metaKey||t.ctrlKey)&&t.key==="f"&&(t.preventDefault(),t.stopPropagation(),this.openSearch()),t.key==="Escape"&&this.searchVisible&&(t.preventDefault(),this.closeSearch())}handleSearchKeyDown(t){t.key==="Enter"&&(t.preventDefault(),t.shiftKey?this.searchPrev():this.searchNext()),t.key==="Escape"&&(t.preventDefault(),this.closeSearch())}createToolbarButton(t,e,s){const i=document.createElement("button");return i.classList.add("edv-toolbar-btn"),i.textContent=t,i.title=e,i.addEventListener("click",n=>{n.stopPropagation(),s()}),i}toggleWordWrap(){this.wordWrap=!this.wordWrap,this.container.classList.toggle("edv-word-wrap",this.wordWrap),this.wrapBtn&&this.wrapBtn.classList.toggle("edv-toolbar-btn--active",this.wordWrap)}isWordWrap(){return this.wordWrap}foldAll(){this.foldState.foldAll(),this.render()}unfoldAll(){this.foldState.unfoldAll(),this.render()}foldToLevel(t){this.foldState.foldToLevel(t),this.render()}getContent(){return this.code}async copyContent(){try{await navigator.clipboard.writeText(this.code)}catch{const t=document.createElement("textarea");t.value=this.code,t.style.position="fixed",t.style.opacity="0",document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}this.showCopyFeedback()}showCopyFeedback(){if(!this.copyBtn)return;this.copyResetTimer&&clearTimeout(this.copyResetTimer);const t="⎘";this.copyBtn.textContent="✓",this.copyBtn.classList.add("edv-toolbar-btn--active"),this.copyBtn.title="Copied!",this.copyResetTimer=setTimeout(()=>{this.copyBtn&&(this.copyBtn.textContent=t,this.copyBtn.classList.remove("edv-toolbar-btn--active"),this.copyBtn.title="Copy"),this.copyResetTimer=null},2e3)}openSearch(){var t;this.searchVisible=!0,(t=this.searchBarEl)==null||t.classList.add("edv-search-bar--visible"),this.searchBtn&&this.searchBtn.classList.add("edv-toolbar-btn--active"),this.searchInputEl&&(this.searchInputEl.focus(),this.searchInputEl.select())}closeSearch(){var t;this.searchVisible=!1,(t=this.searchBarEl)==null||t.classList.remove("edv-search-bar--visible"),this.searchBtn&&this.searchBtn.classList.remove("edv-toolbar-btn--active"),this.searchState.clear(),this.updateSearchInfo(),this.render(),this.container.focus()}toggleSearch(){this.searchVisible?this.closeSearch():this.openSearch()}searchNext(){const t=this.searchState.next();t&&this.revealAndScrollToMatch(t),this.updateSearchInfo(),this.render()}searchPrev(){const t=this.searchState.prev();t&&this.revealAndScrollToMatch(t),this.updateSearchInfo(),this.render()}getSearchState(){return this.searchState}search(t,e){var n;const s=(e==null?void 0:e.caseSensitive)??this.searchState.isCaseSensitive();this.searchState.search(this.lines,t,s),this.searchInputEl&&(this.searchInputEl.value=t),(n=this.searchCaseBtn)==null||n.classList.toggle("edv-search-case-btn--active",s),this.updateSearchInfo();const i=this.searchState.getCurrentMatch();i&&this.revealAndScrollToMatch(i),this.render()}clearSearch(){this.searchState.clear(),this.searchInputEl&&(this.searchInputEl.value=""),this.updateSearchInfo(),this.render()}onSearchInput(){var s;const t=((s=this.searchInputEl)==null?void 0:s.value)??"";this.searchState.search(this.lines,t,this.searchState.isCaseSensitive()),this.updateSearchInfo();const e=this.searchState.getCurrentMatch();e&&this.revealAndScrollToMatch(e),this.render()}toggleCaseSensitive(){var i,n;const t=!this.searchState.isCaseSensitive();(i=this.searchCaseBtn)==null||i.classList.toggle("edv-search-case-btn--active",t);const e=((n=this.searchInputEl)==null?void 0:n.value)??"";this.searchState.search(this.lines,e,t),this.updateSearchInfo();const s=this.searchState.getCurrentMatch();s&&this.revealAndScrollToMatch(s),this.render()}updateSearchInfo(){if(!this.searchInfoEl)return;const t=this.searchState.getMatchCount();if(!this.searchState.getQuery())this.searchInfoEl.textContent="",this.searchInfoEl.classList.remove("edv-search-info--no-results");else if(t===0)this.searchInfoEl.textContent="No results",this.searchInfoEl.classList.add("edv-search-info--no-results");else{const s=this.searchState.getCurrentIndex()+1;this.searchInfoEl.textContent=`${s} of ${t}`,this.searchInfoEl.classList.remove("edv-search-info--no-results")}}revealAndScrollToMatch(t){this.foldState.revealLine(t.line)}scrollToCurrentMatch(){const t=this.scrollEl.querySelector(".edv-search-current");t&&t.scrollIntoView({block:"center",behavior:"smooth"})}openFilter(){var t;this.filterVisible=!0,(t=this.filterBarEl)==null||t.classList.add("edv-filter-bar--visible"),this.filterBtn&&this.filterBtn.classList.add("edv-toolbar-btn--active"),this.updateFilterTags(),this.updateFilterInfo(),this.filterInputEl&&this.filterInputEl.focus()}closeFilter(){var t;this.filterVisible=!1,(t=this.filterBarEl)==null||t.classList.remove("edv-filter-bar--visible"),this.hideFilterDropdown(),this.filterBtn&&this.filterBtn.classList.toggle("edv-toolbar-btn--active",this.filterState.isActive()),this.container.focus()}toggleFilter(){this.filterVisible?this.closeFilter():this.openFilter()}setFilterKeys(t){this.filterState.setKeys(t),this.updateFilterTags(),this.updateFilterInfo(),this.updateFilterBtnState(),this.render()}addFilterKey(t){this.filterState.addKey(t),this.updateFilterTags(),this.updateFilterInfo(),this.updateFilterBtnState(),this.render()}removeFilterKey(t){this.filterState.removeKey(t),this.updateFilterTags(),this.updateFilterInfo(),this.updateFilterBtnState(),this.render()}getFilterKeys(){return this.filterState.getKeys()}getAvailableKeys(){return this.filterState.getAvailableKeys()}clearFilter(){this.filterState.clear(),this.updateFilterTags(),this.updateFilterInfo(),this.updateFilterBtnState(),this.render()}getFilterState(){return this.filterState}getFilteredContent(){if(!this.filterState.isActive())return this.code;const t=[];for(let e=0;e<this.lines.length;e++)this.filterState.isLineFiltered(e)||t.push(this.lines[e]);return t.join(`
4
+ `)}async copyFilteredContent(){const t=this.getFilteredContent();try{await navigator.clipboard.writeText(t)}catch{const e=document.createElement("textarea");e.value=t,e.style.position="fixed",e.style.opacity="0",document.body.appendChild(e),e.select(),document.execCommand("copy"),document.body.removeChild(e)}this.showFilterCopyFeedback()}showFilterCopyFeedback(){this.filterCopyBtn&&(this.filterCopyResetTimer&&clearTimeout(this.filterCopyResetTimer),this.filterCopyBtn.textContent="✓",this.filterCopyBtn.title="Copied!",this.filterCopyResetTimer=setTimeout(()=>{this.filterCopyBtn&&(this.filterCopyBtn.textContent="⎘",this.filterCopyBtn.title="Copy Filtered Content"),this.filterCopyResetTimer=null},2e3))}buildFilterBar(){this.filterBarEl=document.createElement("div"),this.filterBarEl.classList.add("edv-filter-bar");const t=document.createElement("div");t.classList.add("edv-filter-input-wrapper"),this.filterInputEl=document.createElement("input"),this.filterInputEl.type="text",this.filterInputEl.classList.add("edv-filter-input"),this.filterInputEl.placeholder="Filter by key…",this.filterInputEl.addEventListener("input",()=>this.onFilterInput()),this.filterInputEl.addEventListener("keydown",i=>this.handleFilterKeyDown(i)),this.filterInputEl.addEventListener("focus",()=>this.onFilterInput()),t.appendChild(this.filterInputEl),this.filterDropdownEl=document.createElement("div"),this.filterDropdownEl.classList.add("edv-filter-dropdown"),t.appendChild(this.filterDropdownEl),this.filterBarEl.appendChild(t),this.filterTagsEl=document.createElement("div"),this.filterTagsEl.classList.add("edv-filter-tags"),this.filterBarEl.appendChild(this.filterTagsEl),this.filterInfoEl=document.createElement("span"),this.filterInfoEl.classList.add("edv-filter-info"),this.filterBarEl.appendChild(this.filterInfoEl),this.filterCopyBtn=document.createElement("button"),this.filterCopyBtn.classList.add("edv-search-nav-btn"),this.filterCopyBtn.textContent="⎘",this.filterCopyBtn.title="Copy Filtered Content",this.filterCopyBtn.addEventListener("click",i=>{i.stopPropagation(),this.copyFilteredContent()}),this.filterBarEl.appendChild(this.filterCopyBtn);const e=document.createElement("button");e.classList.add("edv-search-nav-btn"),e.textContent="⌫",e.title="Clear All Filters",e.addEventListener("click",i=>{i.stopPropagation(),this.clearFilter()}),this.filterBarEl.appendChild(e);const s=document.createElement("button");s.classList.add("edv-search-nav-btn"),s.textContent="✕",s.title="Close",s.addEventListener("click",i=>{i.stopPropagation(),this.closeFilter()}),this.filterBarEl.appendChild(s),document.addEventListener("click",i=>{var n;(n=this.filterBarEl)!=null&&n.contains(i.target)||this.hideFilterDropdown()}),this.container.appendChild(this.filterBarEl)}onFilterInput(){var n;const t=((n=this.filterInputEl)==null?void 0:n.value.trim().toLowerCase())??"",e=this.filterState.getAvailableKeys(),s=this.filterState.getKeys(),i=e.filter(l=>!s.includes(l)&&(t===""||l.toLowerCase().includes(t)));this.showFilterDropdown(i)}handleFilterKeyDown(t){var i,n,l;const e=this.filterDropdownItems.length,s=(i=this.filterDropdownEl)==null?void 0:i.classList.contains("edv-filter-dropdown--visible");if(t.key==="ArrowDown"&&s&&e>0){t.preventDefault(),this.filterDropdownIndex=Math.min(this.filterDropdownIndex+1,e-1),this.updateFilterDropdownHighlight();return}if(t.key==="ArrowUp"&&s&&e>0){t.preventDefault(),this.filterDropdownIndex=Math.max(this.filterDropdownIndex-1,0),this.updateFilterDropdownHighlight();return}if(t.key==="Enter"){if(t.preventDefault(),s&&this.filterDropdownIndex>=0&&this.filterDropdownIndex<e){const c=this.filterDropdownItems[this.filterDropdownIndex];c&&!this.filterState.hasKey(c)&&(this.addFilterKey(c),this.filterInputEl&&(this.filterInputEl.value=""),this.hideFilterDropdown());return}const o=((n=this.filterInputEl)==null?void 0:n.value.trim())??"";if(o){const d=this.filterState.getAvailableKeys().find(g=>g.toLowerCase()===o.toLowerCase());d&&!this.filterState.hasKey(d)&&(this.addFilterKey(d),this.filterInputEl&&(this.filterInputEl.value=""),this.hideFilterDropdown())}}t.key==="Escape"&&(t.preventDefault(),this.hideFilterDropdown(),(l=this.filterInputEl)!=null&&l.value?this.filterInputEl&&(this.filterInputEl.value=""):this.closeFilter())}updateFilterDropdownHighlight(){if(!this.filterDropdownEl)return;const t=this.filterDropdownEl.querySelectorAll(".edv-filter-dropdown-item");t.forEach((s,i)=>{s.classList.toggle("edv-filter-dropdown-item--active",i===this.filterDropdownIndex)});const e=t[this.filterDropdownIndex];e&&e.scrollIntoView({block:"nearest"})}showFilterDropdown(t){if(this.filterDropdownEl){if(this.filterDropdownEl.innerHTML="",this.filterDropdownItems=t,this.filterDropdownIndex=-1,t.length===0){this.filterDropdownEl.classList.remove("edv-filter-dropdown--visible");return}for(const e of t){const s=document.createElement("div");s.classList.add("edv-filter-dropdown-item"),s.textContent=e,s.addEventListener("mousedown",i=>{i.preventDefault(),i.stopPropagation(),this.addFilterKey(e),this.filterInputEl&&(this.filterInputEl.value="",this.filterInputEl.focus()),this.hideFilterDropdown()}),this.filterDropdownEl.appendChild(s)}this.filterDropdownEl.classList.add("edv-filter-dropdown--visible")}}hideFilterDropdown(){var t;(t=this.filterDropdownEl)==null||t.classList.remove("edv-filter-dropdown--visible")}updateFilterTags(){if(!this.filterTagsEl)return;this.filterTagsEl.innerHTML="";const t=this.filterState.getKeys();for(const e of t){const s=document.createElement("span");s.classList.add("edv-filter-tag");const i=document.createElement("span");i.classList.add("edv-filter-tag-label"),i.textContent=e,s.appendChild(i);const n=document.createElement("button");n.classList.add("edv-filter-tag-remove"),n.textContent="✕",n.title=`Remove filter: ${e}`,n.addEventListener("click",l=>{l.stopPropagation(),this.removeFilterKey(e)}),s.appendChild(n),this.filterTagsEl.appendChild(s)}}updateFilterInfo(){if(!this.filterInfoEl)return;const t=this.filterState.getFilteredCount();t===0?this.filterInfoEl.textContent="":this.filterInfoEl.textContent=`${t} key${t>1?"s":""} hidden`}updateFilterBtnState(){this.filterBtn&&this.filterBtn.classList.toggle("edv-toolbar-btn--active",this.filterState.isActive())}setContent(t){this.code=t,this.lines=t.split(`
5
+ `),this.buildLineOffsets();const{modifiedCode:e,inspectLiterals:s}=H(t);this.inspectLiterals=s;const i=k(e);this.tree=i,this.tokens=x(e,i),this.fixInspectLiteralTokenClasses();const n=F(e,i),l=A(n);this.foldState.setRegions(n,l),this.filterState.detectKeys(i,e),this.defaultFilterKeys.length>0&&!this.filterState.isActive()&&(this.filterState.setKeys(this.defaultFilterKeys),this.updateFilterTags(),this.updateFilterInfo(),this.updateFilterBtnState()),this.defaultFoldLevel>0&&this.foldState.foldToLevel(this.defaultFoldLevel),this.searchState.isActive()&&(this.searchState.search(this.lines,this.searchState.getQuery(),this.searchState.isCaseSensitive()),this.updateSearchInfo()),this.render()}onRender(t){this.onRenderCallback=t}fixInspectLiteralTokenClasses(){if(this.inspectLiterals.length!==0)for(const t of this.inspectLiterals)for(const e of this.tokens)e.from>=t.from&&e.to<=t.to&&(e.classes="tok-inspect-literal")}findInspectLiteral(t,e){for(const s of this.inspectLiterals)if(t>=s.from&&e<=s.to)return s;return null}buildLineOffsets(){this.lineOffsets=[0];for(let t=0;t<this.code.length;t++)this.code[t]===`
6
+ `&&this.lineOffsets.push(t+1)}render(){var i;this.scrollEl.innerHTML="";const t=this.lines.length,e=String(t).length;let s=0;for(;s<t;){if(this.foldState.isLineHidden(s)){s++;continue}if(this.filterState.isLineFiltered(s)){s++;continue}const l=this.foldState.isFoldable(s),o=this.foldState.isFolded(s),c=this.foldState.getRegion(s),d=this.createLineElement(s,e,l,o,c);this.scrollEl.appendChild(d),s++}(i=this.onRenderCallback)==null||i.call(this),this.searchState.isActive()&&this.searchState.getCurrentMatch()&&requestAnimationFrame(()=>this.scrollToCurrentMatch())}createLineElement(t,e,s,i,n){const l=document.createElement("div");l.classList.add("edv-line"),l.dataset.line=String(t),this.searchState.getLineMatches(t).length>0&&l.classList.add("edv-line--has-match");const c=document.createElement("div");c.classList.add("edv-gutter");const d=document.createElement("span");d.classList.add("edv-line-number"),d.textContent=String(t+1).padStart(e," "),c.appendChild(d);const g=document.createElement("span");g.classList.add("edv-fold-indicator"),s&&(g.classList.add("edv-foldable"),g.textContent=i?"▶":"▼",g.addEventListener("click",h=>{h.stopPropagation(),this.foldState.toggle(t),this.render()})),c.appendChild(g),l.appendChild(c);const f=document.createElement("div");return f.classList.add("edv-code"),i&&n?this.renderFoldedLine(f,t,n):this.renderHighlightedLine(f,t),l.appendChild(f),l}renderHighlightedLine(t,e){const s=this.lines[e],i=this.lineOffsets[e],n=i+s.length,l=C(this.tokens,i,n),o=this.searchState.getLineMatches(e);if(s.length===0){t.innerHTML="&nbsp;";return}if(o.length===0){this.renderTokenizedText(t,s,l,i);return}this.renderWithSearchHighlights(t,s,l,o,e)}renderTokenizedText(t,e,s,i){const n=i??0;if(s.length===0){const o=document.createElement("span");o.dataset.from=String(n),o.dataset.to=String(n+e.length),o.textContent=e,t.appendChild(o);return}let l=0;for(const o of s){if(o.from>l){const d=document.createElement("span");d.dataset.from=String(n+l),d.dataset.to=String(n+o.from),d.textContent=e.slice(l,o.from),t.appendChild(d)}const c=document.createElement("span");c.className=o.classes,c.dataset.from=String(n+o.from),c.dataset.to=String(n+o.to),c.textContent=e.slice(o.from,o.to),t.appendChild(c),l=o.to}if(l<e.length){const o=document.createElement("span");o.dataset.from=String(n+l),o.dataset.to=String(n+e.length),o.textContent=e.slice(l),t.appendChild(o)}}renderWithSearchHighlights(t,e,s,i,n){const l=new Array(e.length).fill(null);for(const f of s)for(let h=f.from;h<f.to&&h<e.length;h++)l[h]=f.classes;const o=[],c=new Array(e.length).fill(null);for(const f of i)for(let h=f.from;h<f.to&&h<e.length;h++)c[h]=f;let d=0;for(;d<e.length;){const f=l[d],h=c[d],m=h!==null,v=h?this.searchState.isCurrentMatch(n,h.from):!1;let u=d+1;for(;u<e.length;){const y=l[u],E=c[u],P=E!==null,W=E?this.searchState.isCurrentMatch(n,E.from):!1;if(y!==f||P!==m||W!==v)break;u++}o.push({from:d,to:u,tokenClass:f,isMatch:m,isCurrent:v}),d=u}const g=this.lineOffsets[n];for(const f of o){const h=e.slice(f.from,f.to),m=g+f.from,v=g+f.to;if(f.isMatch){const u=document.createElement("mark");if(u.classList.add("edv-search-match"),f.isCurrent&&u.classList.add("edv-search-current"),u.dataset.from=String(m),u.dataset.to=String(v),f.tokenClass){const y=document.createElement("span");y.className=f.tokenClass,y.textContent=h,u.appendChild(y)}else u.textContent=h;t.appendChild(u)}else if(f.tokenClass){const u=document.createElement("span");u.className=f.tokenClass,u.dataset.from=String(m),u.dataset.to=String(v),u.textContent=h,t.appendChild(u)}else{const u=document.createElement("span");u.dataset.from=String(m),u.dataset.to=String(v),u.textContent=h,t.appendChild(u)}}}renderFoldedLine(t,e,s){const i=this.lines[e],n=this.lineOffsets[e],l=n+i.length,o=C(this.tokens,n,l);let c=0;for(const h of o){if(h.from>c){const v=document.createElement("span");v.dataset.from=String(n+c),v.dataset.to=String(n+h.from),v.textContent=i.slice(c,h.from),t.appendChild(v)}const m=document.createElement("span");m.className=h.classes,m.dataset.from=String(n+h.from),m.dataset.to=String(n+h.to),m.textContent=i.slice(h.from,h.to),t.appendChild(m),c=h.to}if(c<i.length){const h=document.createElement("span");h.dataset.from=String(n+c),h.dataset.to=String(n+i.length),h.textContent=i.slice(c),t.appendChild(h)}const d=document.createElement("span");d.classList.add("edv-fold-ellipsis");const g=s.endLine-s.startLine;if(s.itemCount>0)d.textContent=`${s.itemCount} items`,d.title=`${s.itemCount} items, ${g} lines folded`;else{const m=s.openText==='"""'||s.openText==="'''"?g-1:g;d.textContent=`${m} lines`,d.title=`${m} lines folded`}d.dataset.from=String(s.startOffset),d.dataset.to=String(s.endOffset),d.addEventListener("click",h=>{h.stopPropagation(),this.foldState.toggle(e),this.render()}),t.appendChild(d);const f=document.createElement("span");f.classList.add("tok-punctuation"),f.dataset.from=String(s.endOffset-s.closeText.length),f.dataset.to=String(s.endOffset),f.textContent=s.closeText,t.appendChild(f)}handleInspectHover(t){const e=t.target;if(!e||!this.tree)return;const s=e.closest("[data-from]");if(!s){this.clearInspectHighlight();return}const i=parseInt(s.dataset.from,10);if(isNaN(i))return;const n=N(this.tree,this.code,i);if(!n){this.clearInspectHighlight();return}this.findInspectLiteral(n.from,n.to)&&(n.type="InspectLiteral"),!(this.currentInspect&&this.currentInspect.from===n.from&&this.currentInspect.to===n.to)&&(this.clearInspectHighlight(),this.currentInspect=n,this.applyInspectHighlight(n))}handleInspectOut(t){const s=t.relatedTarget;s&&this.scrollEl.contains(s)||this.clearInspectHighlight()}onInspect(t){this.inspectCallback=t}handleInspectClick(t){if(!this.currentInspect)return;const e=t.target;if(!e)return;const s=e.closest(".edv-fold-indicator, .edv-fold-ellipsis");if(s&&!s.dataset.from)return;const i=e.closest("[data-from]");if(!i)return;const n=this.currentInspect.copyText;let l=!1;if(this.inspectCallback){const o={type:this.currentInspect.type,copyText:n,target:this.currentInspect,element:i,mouseEvent:t,preventDefault(){l=!0}};this.inspectCallback(o)}this.flashInspectHighlight(),l||(this.copyToClipboard(n),this.showInspectToast(t))}applyInspectHighlight(t){if(t.isStructure){const e=this.offsetToLine(t.from),s=this.offsetToLine(t.to-1),i=this.scrollEl.querySelectorAll(".edv-line");for(const n of i){const l=parseInt(n.dataset.line,10);isNaN(l)||l>=e&&l<=s&&n.classList.add("edv-inspect-line")}this.highlightSpansInRange(t.from,t.to,"edv-inspect-bracket")}else this.highlightSpansInRange(t.from,t.to,"edv-inspect-token")}highlightSpansInRange(t,e,s){const i=this.scrollEl.querySelectorAll("[data-from]");for(const n of i){const l=parseInt(n.dataset.from,10),o=parseInt(n.dataset.to,10);isNaN(l)||isNaN(o)||l<e&&o>t&&n.classList.add(s)}}clearInspectHighlight(){if(!this.currentInspect)return;const t=this.scrollEl.querySelectorAll(".edv-inspect-line");for(const s of t)s.classList.remove("edv-inspect-line");const e=this.scrollEl.querySelectorAll(".edv-inspect-token, .edv-inspect-bracket");for(const s of e)s.classList.remove("edv-inspect-token","edv-inspect-bracket");this.currentInspect=null}flashInspectHighlight(){const t=this.scrollEl.querySelectorAll(".edv-inspect-token, .edv-inspect-bracket");for(const e of t)e.classList.add("edv-inspect-copied"),e.addEventListener("animationend",()=>e.classList.remove("edv-inspect-copied"),{once:!0})}showInspectToast(t){const e=document.createElement("div");e.classList.add("edv-copied-toast"),e.textContent="Copied!",e.style.left=`${t.clientX+8}px`,e.style.top=`${t.clientY-24}px`,document.body.appendChild(e),e.addEventListener("animationend",()=>{e.remove()})}async copyToClipboard(t){try{await navigator.clipboard.writeText(t)}catch{const e=document.createElement("textarea");e.value=t,e.style.position="fixed",e.style.opacity="0",document.body.appendChild(e),e.select(),document.execCommand("copy"),document.body.removeChild(e)}}offsetToLine(t){let e=0,s=this.lineOffsets.length-1;for(;e<s;){const i=e+s+1>>1;this.lineOffsets[i]<=t?e=i:s=i-1}return e}}exports.ElixirDataViewer=st;exports.FilterState=D;exports.FoldState=K;exports.SearchState=M;exports.buildFoldMap=A;exports.detectFoldRegions=F;exports.getLineTokens=C;exports.highlight=x;exports.parseElixir=k;exports.preprocessInspectLiterals=H;exports.resolveInspectTarget=N;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Elixir Data Viewer — A read-only web viewer for Elixir data structures
3
+ * with syntax highlighting, code folding, and line numbers.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { ElixirDataViewer } from "elixir-data-viewer";
8
+ * import "elixir-data-viewer/style.css";
9
+ *
10
+ * const viewer = new ElixirDataViewer(document.getElementById("container")!);
11
+ * viewer.setContent('%{name: "Alice", age: 30}');
12
+ * ```
13
+ */
14
+ import "./styles/theme.css";
15
+ export { ElixirDataViewer } from "./renderer";
16
+ export type { ElixirDataViewerOptions, ToolbarOptions, InspectEvent } from "./renderer";
17
+ export { parseElixir } from "./parser";
18
+ export { highlight, getLineTokens } from "./highlighter";
19
+ export type { HighlightToken } from "./highlighter";
20
+ export { detectFoldRegions, buildFoldMap } from "./fold";
21
+ export type { FoldRegion } from "./fold";
22
+ export { FoldState } from "./state";
23
+ export { SearchState } from "./search";
24
+ export type { SearchMatch } from "./search";
25
+ export { FilterState } from "./filter";
26
+ export type { KeyRange } from "./filter";
27
+ export { resolveInspectTarget } from "./inspect";
28
+ export type { InspectTarget, InspectType } from "./inspect";
29
+ export { preprocessInspectLiterals } from "./preprocess";
30
+ export type { InspectLiteral, PreprocessResult } from "./preprocess";