juxscript 1.1.43 → 1.1.45

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.
@@ -2,22 +2,49 @@ import { BaseComponent } from './base/BaseComponent.js';
2
2
  export interface CodeOptions {
3
3
  content?: string;
4
4
  language?: string;
5
+ showLineNumbers?: boolean;
6
+ startLine?: number;
7
+ highlightLines?: number[];
5
8
  style?: string;
6
9
  class?: string;
7
10
  }
8
11
  type CodeState = {
9
12
  content: string;
10
13
  language: string;
14
+ showLineNumbers: boolean;
15
+ startLine: number;
16
+ highlightLines: number[];
11
17
  style: string;
12
18
  class: string;
13
19
  };
14
20
  export declare class Code extends BaseComponent<CodeState> {
21
+ private _codeContainer;
22
+ private static _syntaxCSSInjected;
15
23
  constructor(id: string, options?: CodeOptions);
16
24
  protected getTriggerEvents(): readonly string[];
17
25
  protected getCallbackEvents(): readonly string[];
18
26
  update(prop: string, value: any): void;
19
27
  content(value: string): this;
20
28
  language(value: string): this;
29
+ showLineNumbers(value: boolean): this;
30
+ startLine(value: number): this;
31
+ highlightLines(lines: number[]): this;
32
+ /**
33
+ * Parse code content into individual lines with tokens
34
+ */
35
+ private _parseLines;
36
+ /**
37
+ * Rebuild all line elements
38
+ */
39
+ private _rebuildLines;
40
+ /**
41
+ * Create a single line element with parsed tokens
42
+ */
43
+ private _createLineElement;
44
+ /**
45
+ * Update highlighted lines without full rebuild
46
+ */
47
+ private _updateHighlights;
21
48
  render(targetId?: string | HTMLElement | BaseComponent<any>): this;
22
49
  }
23
50
  export declare function code(id: string, options?: CodeOptions): Code;
@@ -1 +1 @@
1
- {"version":3,"file":"code.d.ts","sourceRoot":"","sources":["code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAMxD,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,SAAS,GAAG;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,qBAAa,IAAK,SAAQ,aAAa,CAAC,SAAS,CAAC;gBACpC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB;IASjD,SAAS,CAAC,gBAAgB,IAAI,SAAS,MAAM,EAAE;IAI/C,SAAS,CAAC,iBAAiB,IAAI,SAAS,MAAM,EAAE;IAIhD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAQtC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK5B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAS7B,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,IAAI;CAmEnE;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAEhE"}
1
+ {"version":3,"file":"code.d.ts","sourceRoot":"","sources":["code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAOxD,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,SAAS,GAAG;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,qBAAa,IAAK,SAAQ,aAAa,CAAC,SAAS,CAAC;IAChD,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAS;gBAE9B,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB;IAYjD,SAAS,CAAC,gBAAgB,IAAI,SAAS,MAAM,EAAE;IAI/C,SAAS,CAAC,iBAAiB,IAAI,SAAS,MAAM,EAAE;IAIhD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAsBtC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK5B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK7B,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAKrC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK9B,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IASrC;;OAEG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,OAAO,CAAC,aAAa;IAyBrB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA0B1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAezB,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,IAAI;CAgFnE;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAEhE"}
@@ -1,4 +1,5 @@
1
1
  import { BaseComponent } from './base/BaseComponent.js';
2
+ import { parseCode, renderLineWithTokens, getSyntaxHighlightCSS } from '../utils/codeparser.js';
2
3
  // Event definitions
3
4
  const TRIGGER_EVENTS = [];
4
5
  const CALLBACK_EVENTS = [];
@@ -7,9 +8,13 @@ export class Code extends BaseComponent {
7
8
  super(id, {
8
9
  content: options.content ?? '',
9
10
  language: options.language ?? 'javascript',
11
+ showLineNumbers: options.showLineNumbers ?? true,
12
+ startLine: options.startLine ?? 1,
13
+ highlightLines: options.highlightLines ?? [],
10
14
  style: options.style ?? '',
11
15
  class: options.class ?? ''
12
16
  });
17
+ this._codeContainer = null;
13
18
  }
14
19
  getTriggerEvents() {
15
20
  return TRIGGER_EVENTS;
@@ -18,7 +23,20 @@ export class Code extends BaseComponent {
18
23
  return CALLBACK_EVENTS;
19
24
  }
20
25
  update(prop, value) {
21
- // No reactive updates needed
26
+ super.update(prop, value);
27
+ if (!this._codeContainer)
28
+ return;
29
+ switch (prop) {
30
+ case 'content':
31
+ this._rebuildLines();
32
+ break;
33
+ case 'showLineNumbers':
34
+ this._codeContainer.classList.toggle('jux-code-no-numbers', !value);
35
+ break;
36
+ case 'highlightLines':
37
+ this._updateHighlights();
38
+ break;
39
+ }
22
40
  }
23
41
  /* ═════════════════════════════════════════════════════════════════
24
42
  * FLUENT API
@@ -31,23 +49,123 @@ export class Code extends BaseComponent {
31
49
  this.state.language = value;
32
50
  return this;
33
51
  }
52
+ showLineNumbers(value) {
53
+ this.state.showLineNumbers = value;
54
+ return this;
55
+ }
56
+ startLine(value) {
57
+ this.state.startLine = value;
58
+ return this;
59
+ }
60
+ highlightLines(lines) {
61
+ this.state.highlightLines = lines;
62
+ return this;
63
+ }
64
+ /* ═════════════════════════════════════════════════════════════════
65
+ * PARSING & RENDERING
66
+ * ═════════════════════════════════════════════════════════════════ */
67
+ /**
68
+ * Parse code content into individual lines with tokens
69
+ */
70
+ _parseLines() {
71
+ return parseCode(this.state.content, this.state.language);
72
+ }
73
+ /**
74
+ * Rebuild all line elements
75
+ */
76
+ _rebuildLines() {
77
+ if (!this._codeContainer)
78
+ return;
79
+ const codeEl = this._codeContainer.querySelector('.jux-code-lines');
80
+ if (!codeEl)
81
+ return;
82
+ codeEl.innerHTML = '';
83
+ const parsedLines = this._parseLines();
84
+ const { startLine, highlightLines, showLineNumbers } = this.state;
85
+ parsedLines.forEach((parsedLine, index) => {
86
+ const lineNumber = startLine + index;
87
+ const isHighlighted = highlightLines.includes(lineNumber);
88
+ const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
89
+ codeEl.appendChild(lineEl);
90
+ });
91
+ // Re-run Prism if available
92
+ if (window.Prism && this._codeContainer) {
93
+ window.Prism.highlightAllUnder(this._codeContainer);
94
+ }
95
+ }
96
+ /**
97
+ * Create a single line element with parsed tokens
98
+ */
99
+ _createLineElement(parsedLine, lineNumber, highlighted) {
100
+ const lineEl = document.createElement('div');
101
+ lineEl.className = 'jux-code-line';
102
+ lineEl.setAttribute('data-line', String(lineNumber));
103
+ if (highlighted) {
104
+ lineEl.classList.add('jux-code-line-highlight');
105
+ }
106
+ // Line number
107
+ if (this.state.showLineNumbers) {
108
+ const lineNum = document.createElement('span');
109
+ lineNum.className = 'jux-code-line-number';
110
+ lineNum.textContent = String(lineNumber);
111
+ lineEl.appendChild(lineNum);
112
+ }
113
+ // Line content with syntax highlighting
114
+ const lineCode = document.createElement('span');
115
+ lineCode.className = 'jux-code-line-content';
116
+ lineCode.innerHTML = renderLineWithTokens(parsedLine);
117
+ lineEl.appendChild(lineCode);
118
+ return lineEl;
119
+ }
120
+ /**
121
+ * Update highlighted lines without full rebuild
122
+ */
123
+ _updateHighlights() {
124
+ if (!this._codeContainer)
125
+ return;
126
+ const lines = this._codeContainer.querySelectorAll('.jux-code-line');
127
+ lines.forEach((line) => {
128
+ const lineNum = parseInt(line.getAttribute('data-line') || '0', 10);
129
+ const isHighlighted = this.state.highlightLines.includes(lineNum);
130
+ line.classList.toggle('jux-code-line-highlight', isHighlighted);
131
+ });
132
+ }
34
133
  /* ═════════════════════════════════════════════════════════════════
35
134
  * RENDER
36
135
  * ═════════════════════════════════════════════════════════════════ */
37
136
  render(targetId) {
38
137
  const container = this._setupContainer(targetId);
39
- const { content, language, style, class: className } = this.state;
138
+ // Inject syntax CSS once globally
139
+ if (!Code._syntaxCSSInjected) {
140
+ const style = document.createElement('style');
141
+ style.id = 'jux-code-syntax-css';
142
+ style.textContent = getSyntaxHighlightCSS();
143
+ document.head.appendChild(style);
144
+ Code._syntaxCSSInjected = true;
145
+ }
146
+ const { language, showLineNumbers, style, class: className } = this.state;
40
147
  const wrapper = document.createElement('div');
41
148
  wrapper.className = 'jux-code';
42
149
  wrapper.id = this._id;
150
+ if (!showLineNumbers)
151
+ wrapper.classList.add('jux-code-no-numbers');
43
152
  if (className)
44
153
  wrapper.className += ` ${className}`;
45
154
  if (style)
46
155
  wrapper.setAttribute('style', style);
47
156
  const pre = document.createElement('pre');
157
+ pre.className = `language-${language}`;
48
158
  const codeEl = document.createElement('code');
49
- codeEl.className = `language-${language}`;
50
- codeEl.textContent = content;
159
+ codeEl.className = 'jux-code-lines';
160
+ // Build initial lines with parsed tokens
161
+ const parsedLines = this._parseLines();
162
+ const { startLine, highlightLines } = this.state;
163
+ parsedLines.forEach((parsedLine, index) => {
164
+ const lineNumber = startLine + index;
165
+ const isHighlighted = highlightLines.includes(lineNumber);
166
+ const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
167
+ codeEl.appendChild(lineEl);
168
+ });
51
169
  pre.appendChild(codeEl);
52
170
  wrapper.appendChild(pre);
53
171
  this._wireStandardEvents(wrapper);
@@ -57,40 +175,34 @@ export class Code extends BaseComponent {
57
175
  const transform = toComponent || ((v) => String(v));
58
176
  stateObj.subscribe((val) => {
59
177
  const transformed = transform(val);
60
- codeEl.textContent = transformed;
61
178
  this.state.content = transformed;
62
- if (window.Prism) {
63
- window.Prism.highlightElement(codeEl);
64
- }
179
+ this._rebuildLines();
65
180
  });
66
181
  }
67
182
  else if (property === 'language') {
68
183
  const transform = toComponent || ((v) => String(v));
69
184
  stateObj.subscribe((val) => {
70
185
  const transformed = transform(val);
71
- codeEl.className = `language-${transformed}`;
72
186
  this.state.language = transformed;
73
- if (window.Prism) {
74
- window.Prism.highlightElement(codeEl);
75
- }
187
+ pre.className = `language-${transformed}`;
188
+ this._rebuildLines();
76
189
  });
77
190
  }
78
- });
79
- container.appendChild(wrapper);
80
- requestAnimationFrame(() => {
81
- if (window.Prism) {
82
- window.Prism.highlightElement(codeEl);
83
- }
84
- else if (process.env.NODE_ENV !== 'production') {
85
- // ✅ Helpful hint in development
86
- console.log(`💡 Tip: Add Prism.js for syntax highlighting:\n` +
87
- ` <link href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css" rel="stylesheet" />\n` +
88
- ` <script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js"></script>`);
191
+ else if (property === 'highlightLines') {
192
+ const transform = toComponent || ((v) => (Array.isArray(v) ? v : []));
193
+ stateObj.subscribe((val) => {
194
+ const transformed = transform(val);
195
+ this.state.highlightLines = transformed;
196
+ this._updateHighlights();
197
+ });
89
198
  }
90
199
  });
200
+ container.appendChild(wrapper);
201
+ this._codeContainer = wrapper;
91
202
  return this;
92
203
  }
93
204
  }
205
+ Code._syntaxCSSInjected = false;
94
206
  export function code(id, options = {}) {
95
207
  return new Code(id, options);
96
208
  }
@@ -1,4 +1,5 @@
1
1
  import { BaseComponent } from './base/BaseComponent.js';
2
+ import { parseCode, renderLineWithTokens, getSyntaxHighlightCSS } from '../utils/codeparser.js';
2
3
 
3
4
  // Event definitions
4
5
  const TRIGGER_EVENTS = [] as const;
@@ -7,6 +8,9 @@ const CALLBACK_EVENTS = [] as const;
7
8
  export interface CodeOptions {
8
9
  content?: string;
9
10
  language?: string;
11
+ showLineNumbers?: boolean;
12
+ startLine?: number;
13
+ highlightLines?: number[];
10
14
  style?: string;
11
15
  class?: string;
12
16
  }
@@ -14,15 +18,24 @@ export interface CodeOptions {
14
18
  type CodeState = {
15
19
  content: string;
16
20
  language: string;
21
+ showLineNumbers: boolean;
22
+ startLine: number;
23
+ highlightLines: number[];
17
24
  style: string;
18
25
  class: string;
19
26
  };
20
27
 
21
28
  export class Code extends BaseComponent<CodeState> {
29
+ private _codeContainer: HTMLElement | null = null;
30
+ private static _syntaxCSSInjected = false;
31
+
22
32
  constructor(id: string, options: CodeOptions = {}) {
23
33
  super(id, {
24
34
  content: options.content ?? '',
25
35
  language: options.language ?? 'javascript',
36
+ showLineNumbers: options.showLineNumbers ?? true,
37
+ startLine: options.startLine ?? 1,
38
+ highlightLines: options.highlightLines ?? [],
26
39
  style: options.style ?? '',
27
40
  class: options.class ?? ''
28
41
  });
@@ -37,7 +50,21 @@ export class Code extends BaseComponent<CodeState> {
37
50
  }
38
51
 
39
52
  update(prop: string, value: any): void {
40
- // No reactive updates needed
53
+ super.update(prop, value);
54
+
55
+ if (!this._codeContainer) return;
56
+
57
+ switch (prop) {
58
+ case 'content':
59
+ this._rebuildLines();
60
+ break;
61
+ case 'showLineNumbers':
62
+ this._codeContainer.classList.toggle('jux-code-no-numbers', !value);
63
+ break;
64
+ case 'highlightLines':
65
+ this._updateHighlights();
66
+ break;
67
+ }
41
68
  }
42
69
 
43
70
  /* ═════════════════════════════════════════════════════════════════
@@ -54,6 +81,103 @@ export class Code extends BaseComponent<CodeState> {
54
81
  return this;
55
82
  }
56
83
 
84
+ showLineNumbers(value: boolean): this {
85
+ this.state.showLineNumbers = value;
86
+ return this;
87
+ }
88
+
89
+ startLine(value: number): this {
90
+ this.state.startLine = value;
91
+ return this;
92
+ }
93
+
94
+ highlightLines(lines: number[]): this {
95
+ this.state.highlightLines = lines;
96
+ return this;
97
+ }
98
+
99
+ /* ═════════════════════════════════════════════════════════════════
100
+ * PARSING & RENDERING
101
+ * ═════════════════════════════════════════════════════════════════ */
102
+
103
+ /**
104
+ * Parse code content into individual lines with tokens
105
+ */
106
+ private _parseLines(): ReturnType<typeof parseCode> {
107
+ return parseCode(this.state.content, this.state.language);
108
+ }
109
+
110
+ /**
111
+ * Rebuild all line elements
112
+ */
113
+ private _rebuildLines(): void {
114
+ if (!this._codeContainer) return;
115
+
116
+ const codeEl = this._codeContainer.querySelector('.jux-code-lines');
117
+ if (!codeEl) return;
118
+
119
+ codeEl.innerHTML = '';
120
+
121
+ const parsedLines = this._parseLines();
122
+ const { startLine, highlightLines, showLineNumbers } = this.state;
123
+
124
+ parsedLines.forEach((parsedLine, index) => {
125
+ const lineNumber = startLine + index;
126
+ const isHighlighted = highlightLines.includes(lineNumber);
127
+
128
+ const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
129
+ codeEl.appendChild(lineEl);
130
+ });
131
+
132
+ // Re-run Prism if available
133
+ if ((window as any).Prism && this._codeContainer) {
134
+ (window as any).Prism.highlightAllUnder(this._codeContainer);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Create a single line element with parsed tokens
140
+ */
141
+ private _createLineElement(parsedLine: ReturnType<typeof parseCode>[0], lineNumber: number, highlighted: boolean): HTMLElement {
142
+ const lineEl = document.createElement('div');
143
+ lineEl.className = 'jux-code-line';
144
+ lineEl.setAttribute('data-line', String(lineNumber));
145
+
146
+ if (highlighted) {
147
+ lineEl.classList.add('jux-code-line-highlight');
148
+ }
149
+
150
+ // Line number
151
+ if (this.state.showLineNumbers) {
152
+ const lineNum = document.createElement('span');
153
+ lineNum.className = 'jux-code-line-number';
154
+ lineNum.textContent = String(lineNumber);
155
+ lineEl.appendChild(lineNum);
156
+ }
157
+
158
+ // Line content with syntax highlighting
159
+ const lineCode = document.createElement('span');
160
+ lineCode.className = 'jux-code-line-content';
161
+ lineCode.innerHTML = renderLineWithTokens(parsedLine);
162
+ lineEl.appendChild(lineCode);
163
+
164
+ return lineEl;
165
+ }
166
+
167
+ /**
168
+ * Update highlighted lines without full rebuild
169
+ */
170
+ private _updateHighlights(): void {
171
+ if (!this._codeContainer) return;
172
+
173
+ const lines = this._codeContainer.querySelectorAll('.jux-code-line');
174
+ lines.forEach((line) => {
175
+ const lineNum = parseInt(line.getAttribute('data-line') || '0', 10);
176
+ const isHighlighted = this.state.highlightLines.includes(lineNum);
177
+ line.classList.toggle('jux-code-line-highlight', isHighlighted);
178
+ });
179
+ }
180
+
57
181
  /* ═════════════════════════════════════════════════════════════════
58
182
  * RENDER
59
183
  * ═════════════════════════════════════════════════════════════════ */
@@ -61,18 +185,41 @@ export class Code extends BaseComponent<CodeState> {
61
185
  render(targetId?: string | HTMLElement | BaseComponent<any>): this {
62
186
  const container = this._setupContainer(targetId);
63
187
 
64
- const { content, language, style, class: className } = this.state;
188
+ // Inject syntax CSS once globally
189
+ if (!Code._syntaxCSSInjected) {
190
+ const style = document.createElement('style');
191
+ style.id = 'jux-code-syntax-css';
192
+ style.textContent = getSyntaxHighlightCSS();
193
+ document.head.appendChild(style);
194
+ Code._syntaxCSSInjected = true;
195
+ }
196
+
197
+ const { language, showLineNumbers, style, class: className } = this.state;
65
198
 
66
199
  const wrapper = document.createElement('div');
67
200
  wrapper.className = 'jux-code';
68
201
  wrapper.id = this._id;
202
+ if (!showLineNumbers) wrapper.classList.add('jux-code-no-numbers');
69
203
  if (className) wrapper.className += ` ${className}`;
70
204
  if (style) wrapper.setAttribute('style', style);
71
205
 
72
206
  const pre = document.createElement('pre');
207
+ pre.className = `language-${language}`;
208
+
73
209
  const codeEl = document.createElement('code');
74
- codeEl.className = `language-${language}`;
75
- codeEl.textContent = content;
210
+ codeEl.className = 'jux-code-lines';
211
+
212
+ // Build initial lines with parsed tokens
213
+ const parsedLines = this._parseLines();
214
+ const { startLine, highlightLines } = this.state;
215
+
216
+ parsedLines.forEach((parsedLine, index) => {
217
+ const lineNumber = startLine + index;
218
+ const isHighlighted = highlightLines.includes(lineNumber);
219
+ const lineEl = this._createLineElement(parsedLine, lineNumber, isHighlighted);
220
+ codeEl.appendChild(lineEl);
221
+ });
222
+
76
223
  pre.appendChild(codeEl);
77
224
  wrapper.appendChild(pre);
78
225
 
@@ -85,12 +232,8 @@ export class Code extends BaseComponent<CodeState> {
85
232
 
86
233
  stateObj.subscribe((val: any) => {
87
234
  const transformed = transform(val);
88
- codeEl.textContent = transformed;
89
235
  this.state.content = transformed;
90
-
91
- if ((window as any).Prism) {
92
- (window as any).Prism.highlightElement(codeEl);
93
- }
236
+ this._rebuildLines();
94
237
  });
95
238
  }
96
239
  else if (property === 'language') {
@@ -98,30 +241,24 @@ export class Code extends BaseComponent<CodeState> {
98
241
 
99
242
  stateObj.subscribe((val: any) => {
100
243
  const transformed = transform(val);
101
- codeEl.className = `language-${transformed}`;
102
244
  this.state.language = transformed;
245
+ pre.className = `language-${transformed}`;
246
+ this._rebuildLines();
247
+ });
248
+ }
249
+ else if (property === 'highlightLines') {
250
+ const transform = toComponent || ((v: any) => (Array.isArray(v) ? v : []));
103
251
 
104
- if ((window as any).Prism) {
105
- (window as any).Prism.highlightElement(codeEl);
106
- }
252
+ stateObj.subscribe((val: any) => {
253
+ const transformed = transform(val);
254
+ this.state.highlightLines = transformed;
255
+ this._updateHighlights();
107
256
  });
108
257
  }
109
258
  });
110
259
 
111
260
  container.appendChild(wrapper);
112
-
113
- requestAnimationFrame(() => {
114
- if ((window as any).Prism) {
115
- (window as any).Prism.highlightElement(codeEl);
116
- } else if (process.env.NODE_ENV !== 'production') {
117
- // ✅ Helpful hint in development
118
- console.log(
119
- `💡 Tip: Add Prism.js for syntax highlighting:\n` +
120
- ` <link href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css" rel="stylesheet" />\n` +
121
- ` <script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js"></script>`
122
- );
123
- }
124
- });
261
+ this._codeContainer = wrapper;
125
262
 
126
263
  return this;
127
264
  }
@@ -0,0 +1,28 @@
1
+ export interface ParsedToken {
2
+ type: string;
3
+ value: string;
4
+ start: number;
5
+ end: number;
6
+ line: number;
7
+ column: number;
8
+ className: string;
9
+ }
10
+ export interface ParsedLine {
11
+ lineNumber: number;
12
+ tokens: ParsedToken[];
13
+ raw: string;
14
+ }
15
+ /**
16
+ * Parse JavaScript/TypeScript code using Acorn
17
+ * Returns token-level information for syntax highlighting
18
+ */
19
+ export declare function parseCode(code: string, language?: string): ParsedLine[];
20
+ /**
21
+ * Render a parsed line as HTML with spans
22
+ */
23
+ export declare function renderLineWithTokens(parsedLine: ParsedLine): string;
24
+ /**
25
+ * Generate CSS for syntax highlighting
26
+ */
27
+ export declare function getSyntaxHighlightCSS(): string;
28
+ //# sourceMappingURL=codeparser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codeparser.d.ts","sourceRoot":"","sources":["codeparser.ts"],"names":[],"mappings":"AA4CA,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAqB,GAAG,UAAU,EAAE,CA0ErF;AA+BD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAiCnE;AAcD;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAmB9C"}
@@ -0,0 +1,198 @@
1
+ import * as acorn from 'acorn';
2
+ /**
3
+ * Token type to CSS class mapping
4
+ */
5
+ const TOKEN_CLASS_MAP = {
6
+ // Keywords
7
+ 'Keyword': 'token-keyword',
8
+ 'this': 'token-keyword',
9
+ 'const': 'token-keyword',
10
+ 'let': 'token-keyword',
11
+ 'var': 'token-keyword',
12
+ 'function': 'token-keyword',
13
+ 'return': 'token-keyword',
14
+ 'if': 'token-keyword',
15
+ 'else': 'token-keyword',
16
+ 'for': 'token-keyword',
17
+ 'while': 'token-keyword',
18
+ 'class': 'token-keyword',
19
+ 'import': 'token-keyword',
20
+ 'export': 'token-keyword',
21
+ 'from': 'token-keyword',
22
+ 'async': 'token-keyword',
23
+ 'await': 'token-keyword',
24
+ // Literals
25
+ 'String': 'token-string',
26
+ 'Numeric': 'token-number',
27
+ 'Boolean': 'token-boolean',
28
+ 'Null': 'token-null',
29
+ 'RegExp': 'token-regex',
30
+ // Identifiers
31
+ 'Identifier': 'token-identifier',
32
+ 'PrivateIdentifier': 'token-identifier',
33
+ // Operators
34
+ 'Punctuator': 'token-punctuation',
35
+ // Comments
36
+ 'LineComment': 'token-comment',
37
+ 'BlockComment': 'token-comment',
38
+ };
39
+ /**
40
+ * Parse JavaScript/TypeScript code using Acorn
41
+ * Returns token-level information for syntax highlighting
42
+ */
43
+ export function parseCode(code, language = 'javascript') {
44
+ const lines = [];
45
+ const codeLines = code.split('\n');
46
+ try {
47
+ // Parse with Acorn (supports ES2020+)
48
+ const tokens = [];
49
+ acorn.parse(code, {
50
+ ecmaVersion: 2022,
51
+ sourceType: 'module',
52
+ locations: true,
53
+ ranges: true,
54
+ onToken: tokens,
55
+ onComment: (block, text, start, end, startLoc, endLoc) => {
56
+ tokens.push({
57
+ type: block ? 'BlockComment' : 'LineComment',
58
+ value: text,
59
+ start,
60
+ end,
61
+ loc: { start: startLoc, end: endLoc }
62
+ });
63
+ }
64
+ });
65
+ // Sort tokens by position
66
+ tokens.sort((a, b) => a.start - b.start);
67
+ // Group tokens by line
68
+ const tokensByLine = new Map();
69
+ tokens.forEach(token => {
70
+ const line = token.loc.start.line;
71
+ const parsedToken = {
72
+ type: token.type.label || token.type,
73
+ value: token.value !== undefined ? String(token.value) : code.substring(token.start, token.end),
74
+ start: token.start,
75
+ end: token.end,
76
+ line: token.loc.start.line,
77
+ column: token.loc.start.column,
78
+ className: getTokenClass(token)
79
+ };
80
+ if (!tokensByLine.has(line)) {
81
+ tokensByLine.set(line, []);
82
+ }
83
+ tokensByLine.get(line).push(parsedToken);
84
+ });
85
+ // Build lines with tokens
86
+ codeLines.forEach((lineText, index) => {
87
+ const lineNumber = index + 1;
88
+ const lineTokens = tokensByLine.get(lineNumber) || [];
89
+ lines.push({
90
+ lineNumber,
91
+ tokens: lineTokens,
92
+ raw: lineText
93
+ });
94
+ });
95
+ }
96
+ catch (error) {
97
+ // Fallback: if parsing fails, return plain lines
98
+ console.warn('[CodeParser] Parse failed, using plain text:', error);
99
+ codeLines.forEach((lineText, index) => {
100
+ lines.push({
101
+ lineNumber: index + 1,
102
+ tokens: [],
103
+ raw: lineText
104
+ });
105
+ });
106
+ }
107
+ return lines;
108
+ }
109
+ /**
110
+ * Get CSS class for a token
111
+ */
112
+ function getTokenClass(token) {
113
+ const type = token.type.label || token.type;
114
+ // Check if it's a keyword
115
+ if (type === 'name' && token.value && isKeyword(token.value)) {
116
+ return TOKEN_CLASS_MAP[token.value] || TOKEN_CLASS_MAP['Keyword'];
117
+ }
118
+ // Map type to class
119
+ return TOKEN_CLASS_MAP[type] || 'token-default';
120
+ }
121
+ /**
122
+ * Check if a word is a JavaScript keyword
123
+ */
124
+ function isKeyword(word) {
125
+ const keywords = [
126
+ 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
127
+ 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for',
128
+ 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'return',
129
+ 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
130
+ 'while', 'with', 'yield', 'async', 'of', 'static', 'get', 'set'
131
+ ];
132
+ return keywords.includes(word);
133
+ }
134
+ /**
135
+ * Render a parsed line as HTML with spans
136
+ */
137
+ export function renderLineWithTokens(parsedLine) {
138
+ if (parsedLine.tokens.length === 0) {
139
+ // No tokens, return plain text (escaped)
140
+ return escapeHtml(parsedLine.raw) || ' ';
141
+ }
142
+ const { raw, tokens } = parsedLine;
143
+ let html = '';
144
+ let lastIndex = 0;
145
+ // Calculate line start position in original code
146
+ const lineStartPos = tokens[0]?.start - tokens[0]?.column;
147
+ tokens.forEach(token => {
148
+ // Add any whitespace/text between tokens
149
+ const localStart = token.start - lineStartPos;
150
+ if (localStart > lastIndex) {
151
+ html += escapeHtml(raw.substring(lastIndex, localStart));
152
+ }
153
+ // Add token with span
154
+ const localEnd = token.end - lineStartPos;
155
+ const tokenText = raw.substring(localStart, localEnd);
156
+ html += `<span class="${token.className}">${escapeHtml(tokenText)}</span>`;
157
+ lastIndex = localEnd;
158
+ });
159
+ // Add any remaining text
160
+ if (lastIndex < raw.length) {
161
+ html += escapeHtml(raw.substring(lastIndex));
162
+ }
163
+ return html || ' ';
164
+ }
165
+ /**
166
+ * Escape HTML entities
167
+ */
168
+ function escapeHtml(text) {
169
+ return text
170
+ .replace(/&/g, '&amp;')
171
+ .replace(/</g, '&lt;')
172
+ .replace(/>/g, '&gt;')
173
+ .replace(/"/g, '&quot;')
174
+ .replace(/'/g, '&#39;');
175
+ }
176
+ /**
177
+ * Generate CSS for syntax highlighting
178
+ */
179
+ export function getSyntaxHighlightCSS() {
180
+ return `
181
+ /* Code Parser Syntax Highlighting */
182
+ .token-keyword { color: #c678dd; font-weight: 600; }
183
+ .token-string { color: #98c379; }
184
+ .token-number { color: #d19a66; }
185
+ .token-boolean { color: #d19a66; }
186
+ .token-null { color: #d19a66; }
187
+ .token-regex { color: #e06c75; }
188
+ .token-identifier { color: #e5c07b; }
189
+ .token-punctuation { color: #abb2bf; }
190
+ .token-comment { color: #5c6370; font-style: italic; }
191
+ .token-default { color: #abb2bf; }
192
+
193
+ /* Function calls */
194
+ .token-identifier + .token-punctuation:has(+ .token-punctuation) {
195
+ color: #61afef; /* Function names in blue */
196
+ }
197
+ `;
198
+ }
@@ -0,0 +1,242 @@
1
+ import * as acorn from 'acorn';
2
+
3
+ /**
4
+ * Token type to CSS class mapping
5
+ */
6
+ const TOKEN_CLASS_MAP: Record<string, string> = {
7
+ // Keywords
8
+ 'Keyword': 'token-keyword',
9
+ 'this': 'token-keyword',
10
+ 'const': 'token-keyword',
11
+ 'let': 'token-keyword',
12
+ 'var': 'token-keyword',
13
+ 'function': 'token-keyword',
14
+ 'return': 'token-keyword',
15
+ 'if': 'token-keyword',
16
+ 'else': 'token-keyword',
17
+ 'for': 'token-keyword',
18
+ 'while': 'token-keyword',
19
+ 'class': 'token-keyword',
20
+ 'import': 'token-keyword',
21
+ 'export': 'token-keyword',
22
+ 'from': 'token-keyword',
23
+ 'async': 'token-keyword',
24
+ 'await': 'token-keyword',
25
+
26
+ // Literals
27
+ 'String': 'token-string',
28
+ 'Numeric': 'token-number',
29
+ 'Boolean': 'token-boolean',
30
+ 'Null': 'token-null',
31
+ 'RegExp': 'token-regex',
32
+
33
+ // Identifiers
34
+ 'Identifier': 'token-identifier',
35
+ 'PrivateIdentifier': 'token-identifier',
36
+
37
+ // Operators
38
+ 'Punctuator': 'token-punctuation',
39
+
40
+ // Comments
41
+ 'LineComment': 'token-comment',
42
+ 'BlockComment': 'token-comment',
43
+ };
44
+
45
+ export interface ParsedToken {
46
+ type: string;
47
+ value: string;
48
+ start: number;
49
+ end: number;
50
+ line: number;
51
+ column: number;
52
+ className: string;
53
+ }
54
+
55
+ export interface ParsedLine {
56
+ lineNumber: number;
57
+ tokens: ParsedToken[];
58
+ raw: string;
59
+ }
60
+
61
+ /**
62
+ * Parse JavaScript/TypeScript code using Acorn
63
+ * Returns token-level information for syntax highlighting
64
+ */
65
+ export function parseCode(code: string, language: string = 'javascript'): ParsedLine[] {
66
+ const lines: ParsedLine[] = [];
67
+ const codeLines = code.split('\n');
68
+
69
+ try {
70
+ // Parse with Acorn (supports ES2020+)
71
+ const tokens: any[] = [];
72
+
73
+ acorn.parse(code, {
74
+ ecmaVersion: 2022,
75
+ sourceType: 'module',
76
+ locations: true,
77
+ ranges: true,
78
+ onToken: tokens,
79
+ onComment: (block, text, start, end, startLoc, endLoc) => {
80
+ tokens.push({
81
+ type: block ? 'BlockComment' : 'LineComment',
82
+ value: text,
83
+ start,
84
+ end,
85
+ loc: { start: startLoc, end: endLoc }
86
+ });
87
+ }
88
+ });
89
+
90
+ // Sort tokens by position
91
+ tokens.sort((a, b) => a.start - b.start);
92
+
93
+ // Group tokens by line
94
+ const tokensByLine: Map<number, ParsedToken[]> = new Map();
95
+
96
+ tokens.forEach(token => {
97
+ const line = token.loc.start.line;
98
+ const parsedToken: ParsedToken = {
99
+ type: token.type.label || token.type,
100
+ value: token.value !== undefined ? String(token.value) : code.substring(token.start, token.end),
101
+ start: token.start,
102
+ end: token.end,
103
+ line: token.loc.start.line,
104
+ column: token.loc.start.column,
105
+ className: getTokenClass(token)
106
+ };
107
+
108
+ if (!tokensByLine.has(line)) {
109
+ tokensByLine.set(line, []);
110
+ }
111
+ tokensByLine.get(line)!.push(parsedToken);
112
+ });
113
+
114
+ // Build lines with tokens
115
+ codeLines.forEach((lineText, index) => {
116
+ const lineNumber = index + 1;
117
+ const lineTokens = tokensByLine.get(lineNumber) || [];
118
+
119
+ lines.push({
120
+ lineNumber,
121
+ tokens: lineTokens,
122
+ raw: lineText
123
+ });
124
+ });
125
+
126
+ } catch (error) {
127
+ // Fallback: if parsing fails, return plain lines
128
+ console.warn('[CodeParser] Parse failed, using plain text:', error);
129
+ codeLines.forEach((lineText, index) => {
130
+ lines.push({
131
+ lineNumber: index + 1,
132
+ tokens: [],
133
+ raw: lineText
134
+ });
135
+ });
136
+ }
137
+
138
+ return lines;
139
+ }
140
+
141
+ /**
142
+ * Get CSS class for a token
143
+ */
144
+ function getTokenClass(token: any): string {
145
+ const type = token.type.label || token.type;
146
+
147
+ // Check if it's a keyword
148
+ if (type === 'name' && token.value && isKeyword(token.value)) {
149
+ return TOKEN_CLASS_MAP[token.value] || TOKEN_CLASS_MAP['Keyword'];
150
+ }
151
+
152
+ // Map type to class
153
+ return TOKEN_CLASS_MAP[type] || 'token-default';
154
+ }
155
+
156
+ /**
157
+ * Check if a word is a JavaScript keyword
158
+ */
159
+ function isKeyword(word: string): boolean {
160
+ const keywords = [
161
+ 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
162
+ 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for',
163
+ 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'return',
164
+ 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
165
+ 'while', 'with', 'yield', 'async', 'of', 'static', 'get', 'set'
166
+ ];
167
+ return keywords.includes(word);
168
+ }
169
+
170
+ /**
171
+ * Render a parsed line as HTML with spans
172
+ */
173
+ export function renderLineWithTokens(parsedLine: ParsedLine): string {
174
+ if (parsedLine.tokens.length === 0) {
175
+ // No tokens, return plain text (escaped)
176
+ return escapeHtml(parsedLine.raw) || ' ';
177
+ }
178
+
179
+ const { raw, tokens } = parsedLine;
180
+ let html = '';
181
+ let lastIndex = 0;
182
+
183
+ // Calculate line start position in original code
184
+ const lineStartPos = tokens[0]?.start - tokens[0]?.column;
185
+
186
+ tokens.forEach(token => {
187
+ // Add any whitespace/text between tokens
188
+ const localStart = token.start - lineStartPos;
189
+ if (localStart > lastIndex) {
190
+ html += escapeHtml(raw.substring(lastIndex, localStart));
191
+ }
192
+
193
+ // Add token with span
194
+ const localEnd = token.end - lineStartPos;
195
+ const tokenText = raw.substring(localStart, localEnd);
196
+ html += `<span class="${token.className}">${escapeHtml(tokenText)}</span>`;
197
+ lastIndex = localEnd;
198
+ });
199
+
200
+ // Add any remaining text
201
+ if (lastIndex < raw.length) {
202
+ html += escapeHtml(raw.substring(lastIndex));
203
+ }
204
+
205
+ return html || ' ';
206
+ }
207
+
208
+ /**
209
+ * Escape HTML entities
210
+ */
211
+ function escapeHtml(text: string): string {
212
+ return text
213
+ .replace(/&/g, '&amp;')
214
+ .replace(/</g, '&lt;')
215
+ .replace(/>/g, '&gt;')
216
+ .replace(/"/g, '&quot;')
217
+ .replace(/'/g, '&#39;');
218
+ }
219
+
220
+ /**
221
+ * Generate CSS for syntax highlighting
222
+ */
223
+ export function getSyntaxHighlightCSS(): string {
224
+ return `
225
+ /* Code Parser Syntax Highlighting */
226
+ .token-keyword { color: #c678dd; font-weight: 600; }
227
+ .token-string { color: #98c379; }
228
+ .token-number { color: #d19a66; }
229
+ .token-boolean { color: #d19a66; }
230
+ .token-null { color: #d19a66; }
231
+ .token-regex { color: #e06c75; }
232
+ .token-identifier { color: #e5c07b; }
233
+ .token-punctuation { color: #abb2bf; }
234
+ .token-comment { color: #5c6370; font-style: italic; }
235
+ .token-default { color: #abb2bf; }
236
+
237
+ /* Function calls */
238
+ .token-identifier + .token-punctuation:has(+ .token-punctuation) {
239
+ color: #61afef; /* Function names in blue */
240
+ }
241
+ `;
242
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.43",
3
+ "version": "1.1.45",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",