@codady/coax 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +331 -166
- package/dist/coax.cjs.js +72 -57
- package/dist/coax.cjs.min.js +4 -4
- package/dist/coax.esm.js +72 -57
- package/dist/coax.esm.min.js +4 -4
- package/dist/coax.umd.js +243 -229
- package/dist/coax.umd.min.js +4 -4
- package/examples/deepseek-highlight.html +19 -10
- package/package.json +2 -2
- package/script-note.js +2 -2
- package/src/Coax.js +2 -3
- package/src/Coax.ts +2 -3
- package/src/components/{CoaxElem.js → Coax.js} +143 -72
- package/src/components/{CoaxElem.ts → Coax.ts} +151 -83
- package/src/modules.js +3 -3
- package/src/modules.ts +3 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Last modified: 2026/01/12 09:
|
|
2
|
+
* Last modified: 2026/01/12 14:09:42
|
|
3
|
+
* Coax - A custom web component for syntax highlighting, code display, and interactive features
|
|
4
|
+
*
|
|
3
5
|
*/
|
|
4
6
|
import typeWriter from "@codady/utils/typeWriter";
|
|
5
7
|
import parseClasses from "@codady/utils/parseClasses";
|
|
@@ -7,36 +9,34 @@ import NAMESPACE from "@codady/utils/namespace";
|
|
|
7
9
|
import getEl from "@codady/utils/getEl";
|
|
8
10
|
import createTools from "@codady/utils/createTools";
|
|
9
11
|
import createEl from "@codady/utils/createEl";
|
|
10
|
-
class
|
|
12
|
+
class Coax extends HTMLElement {
|
|
11
13
|
// A static Map to hold the configuration for different languages
|
|
12
14
|
static languages = new Map();
|
|
15
|
+
// A static array to hold the tools registered with the component
|
|
13
16
|
static tools = [];
|
|
14
|
-
source;
|
|
15
|
-
_renderQueued = false;
|
|
16
|
-
baseStylesEl;
|
|
17
|
-
themeStylesEl;
|
|
18
|
-
dynamicStylesEl;
|
|
19
|
-
highlightedCodeEl;
|
|
20
|
-
headerEl;
|
|
21
|
-
codeNameEl;
|
|
22
|
-
codeToolsEl;
|
|
23
|
-
codeBodyEl;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
source; // Source code content to be highlighted
|
|
18
|
+
_renderQueued = false; // Flag to prevent multiple render requests
|
|
19
|
+
baseStylesEl; // Element for base styles
|
|
20
|
+
themeStylesEl; // Element for theme styles
|
|
21
|
+
dynamicStylesEl; // Element for dynamic styles
|
|
22
|
+
highlightedCodeEl; // Element that holds the highlighted code
|
|
23
|
+
headerEl; // Header element (for code name, tools, etc.)
|
|
24
|
+
codeNameEl; // Code name element (shows language or alias)
|
|
25
|
+
codeToolsEl; // Code tools element (for interactive tools like copy)
|
|
26
|
+
codeBodyEl; // Code body element (the container for the code)
|
|
27
|
+
lang = 'plain'; // Language of the code (default is plain text)
|
|
28
|
+
alias = 'Plain Text'; // Alias name for the language (default is 'Plain Text')
|
|
29
|
+
lineString = ''; // The current line's string being typed
|
|
30
|
+
lastLineString = ''; // The last line's string
|
|
31
|
+
speed = 5; // Speed of the typing effect (higher is slower)
|
|
32
|
+
autoScroll = true; // Flag to enable/disable auto-scrolling
|
|
28
33
|
constructor() {
|
|
29
34
|
super();
|
|
30
35
|
// Attach a Shadow DOM to the custom element
|
|
31
36
|
this.attachShadow({ mode: 'open' });
|
|
32
37
|
// Remove leading/trailing whitespace from the raw code content
|
|
33
38
|
this.source = this.textContent?.replace(/^\s*\n|\n\s*$/g, '') || '';
|
|
34
|
-
|
|
35
|
-
this.alias = 'Plain Text';
|
|
36
|
-
this.lineString = '';
|
|
37
|
-
this.speed = 5;
|
|
38
|
-
this.autoScroll = true;
|
|
39
|
-
// 1. 初始化基础骨架
|
|
39
|
+
// Initialize the basic structure of the component
|
|
40
40
|
this.shadowRoot.innerHTML = `
|
|
41
41
|
<style id="base-styles">
|
|
42
42
|
:host {
|
|
@@ -169,7 +169,7 @@ class CoaxElem extends HTMLElement {
|
|
|
169
169
|
<pre><code id="highlight-code"></code></pre>
|
|
170
170
|
</div>
|
|
171
171
|
`;
|
|
172
|
-
//
|
|
172
|
+
// Cache references to various elements
|
|
173
173
|
this.baseStylesEl = getEl('#base-styles', this.shadowRoot);
|
|
174
174
|
this.themeStylesEl = getEl('#theme-styles', this.shadowRoot);
|
|
175
175
|
this.dynamicStylesEl = getEl('#dynamic-styles', this.shadowRoot);
|
|
@@ -180,24 +180,30 @@ class CoaxElem extends HTMLElement {
|
|
|
180
180
|
this.highlightedCodeEl = getEl('#highlight-code', this.shadowRoot);
|
|
181
181
|
this.codeBodyEl.addEventListener('scroll', () => {
|
|
182
182
|
let flag = this.codeBodyEl.scrollTop + this.codeBodyEl.clientHeight < this.codeBodyEl.scrollHeight;
|
|
183
|
-
//
|
|
183
|
+
// Check if the user manually scrolled
|
|
184
184
|
this.autoScroll = !flag;
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
* Registers a new language with a set of syntax highlighting rules.
|
|
189
|
+
* @param name - The name of the programming language (e.g., 'javascript', 'html', etc.)
|
|
190
|
+
* @param config - Configuration for the language, including rules, theme, and optional CSS.
|
|
191
|
+
*/
|
|
192
192
|
static register(name, config) {
|
|
193
193
|
// Store the language configuration in the static map
|
|
194
194
|
this.languages.set(name, { ...config });
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Registers tools that can be used with the code editor (e.g., copy, download, etc.).
|
|
198
|
+
* @param items - An array of tool items to register.
|
|
199
|
+
*/
|
|
197
200
|
static addTools(items) {
|
|
198
|
-
|
|
201
|
+
Coax.tools = items;
|
|
199
202
|
}
|
|
200
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Mounts the tools to the code tools container.
|
|
205
|
+
* @param toolItems - An array of tool items to be added to the tools container.
|
|
206
|
+
*/
|
|
201
207
|
mountTools(toolItems) {
|
|
202
208
|
requestAnimationFrame(() => {
|
|
203
209
|
this.codeToolsEl.innerHTML = '';
|
|
@@ -208,15 +214,22 @@ class CoaxElem extends HTMLElement {
|
|
|
208
214
|
this.codeToolsEl.appendChild(createTools(items));
|
|
209
215
|
});
|
|
210
216
|
}
|
|
211
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Called when the element is connected to the DOM.
|
|
219
|
+
*/
|
|
212
220
|
connectedCallback() {
|
|
213
221
|
this.render();
|
|
214
222
|
}
|
|
215
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Observed attributes for changes. These include the language, height, tools, and speed.
|
|
225
|
+
*/
|
|
216
226
|
static get observedAttributes() { return ['lang', 'height', 'max-height', 'tools', 'speed']; }
|
|
217
227
|
/**
|
|
218
|
-
|
|
219
|
-
|
|
228
|
+
* Called when any of the observed attributes change.
|
|
229
|
+
* @param name - The name of the changed attribute.
|
|
230
|
+
* @param oldVal - The old value of the attribute.
|
|
231
|
+
* @param newVal - The new value of the attribute.
|
|
232
|
+
*/
|
|
220
233
|
attributeChangedCallback(name, oldVal, newVal) {
|
|
221
234
|
if (oldVal === newVal)
|
|
222
235
|
return;
|
|
@@ -224,25 +237,28 @@ class CoaxElem extends HTMLElement {
|
|
|
224
237
|
this.updateStyleByRegExp(name, newVal);
|
|
225
238
|
}
|
|
226
239
|
if (name === 'speed') {
|
|
240
|
+
// Convert to integer (0 or 1)
|
|
227
241
|
this.speed = ~~(!!newVal);
|
|
228
242
|
}
|
|
229
243
|
if (name === 'lang') {
|
|
230
|
-
|
|
244
|
+
// Update the language and re-render
|
|
231
245
|
this.lang = newVal;
|
|
232
246
|
this.render();
|
|
233
247
|
}
|
|
234
248
|
if (name === 'tools') {
|
|
235
249
|
if (!newVal)
|
|
236
250
|
this.codeToolsEl.innerHTML = '';
|
|
237
|
-
const tools = parseClasses(newVal), toolItems =
|
|
251
|
+
const tools = parseClasses(newVal), toolItems = Coax.tools.filter(k => tools.includes(k.name));
|
|
238
252
|
if (!toolItems.length)
|
|
239
253
|
return;
|
|
240
254
|
this.mountTools(toolItems);
|
|
241
255
|
}
|
|
242
256
|
}
|
|
243
257
|
/**
|
|
244
|
-
|
|
245
|
-
|
|
258
|
+
* Updates the base style by replacing specific CSS properties using a regular expression.
|
|
259
|
+
* @param prop - The CSS property name to update (e.g., 'height', 'max-height').
|
|
260
|
+
* @param value - The new value for the property.
|
|
261
|
+
*/
|
|
246
262
|
updateStyleByRegExp(prop, value) {
|
|
247
263
|
// 构建正则:匹配属性名后面跟着冒号,直到分号或换行
|
|
248
264
|
// 例如:height:\s*[^;]+;
|
|
@@ -250,11 +266,22 @@ class CoaxElem extends HTMLElement {
|
|
|
250
266
|
// 替换为新的属性值
|
|
251
267
|
this.baseStylesEl.textContent = this.baseStylesEl.textContent.replace(regex, `;\n${prop}: ${value};`);
|
|
252
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Retrieves the CSS prefix for the language configuration.
|
|
271
|
+
* @param config - The language configuration object.
|
|
272
|
+
* @returns The CSS prefix.
|
|
273
|
+
*/
|
|
253
274
|
getCssPrefix(config) {
|
|
254
275
|
return (config?.cssPrefix || this.lang).replace(/[^a-zA-Z0-9_-]/g, '\\$&');
|
|
255
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Highlights a given string according to the language configuration.
|
|
279
|
+
* @param string - The string to highlight.
|
|
280
|
+
* @param config - The language configuration object.
|
|
281
|
+
* @returns The highlighted string with HTML spans.
|
|
282
|
+
*/
|
|
256
283
|
getHighLightString(string, config) {
|
|
257
|
-
config = config ||
|
|
284
|
+
config = config || Coax.languages.get(this.lang);
|
|
258
285
|
if (!config)
|
|
259
286
|
return string;
|
|
260
287
|
// 如果找到了配置,则进行高亮处理
|
|
@@ -266,6 +293,12 @@ class CoaxElem extends HTMLElement {
|
|
|
266
293
|
return i !== -1 && config.rules[i] ? `<span class="${NAMESPACE}-${cssPrefix}-${config.rules[i].token}">${match}</span>` : match;
|
|
267
294
|
});
|
|
268
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Creates a wrapper element for a line of code.
|
|
298
|
+
* @param index - The line index to assign.
|
|
299
|
+
* @param startIndex - The starting index for the line.
|
|
300
|
+
* @returns A div element wrapping the line of code.
|
|
301
|
+
*/
|
|
269
302
|
createLineWrap(index, startIndex) {
|
|
270
303
|
let dataIndex = 0;
|
|
271
304
|
if (index == null && startIndex == null) {
|
|
@@ -277,19 +310,25 @@ class CoaxElem extends HTMLElement {
|
|
|
277
310
|
}
|
|
278
311
|
return createEl('div', { 'data-index': dataIndex }, '<div></div>');
|
|
279
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Fills a line of code with highlighted content.
|
|
315
|
+
* @param codeWrap - The element that will contain the highlighted line of code.
|
|
316
|
+
* @param line - The line of code to highlight.
|
|
317
|
+
* @param config - The language configuration object.
|
|
318
|
+
*/
|
|
280
319
|
getLineToFill(codeWrap, line, config) {
|
|
281
|
-
config = config ||
|
|
320
|
+
config = config || Coax.languages.get(this.lang);
|
|
282
321
|
let highlightedLine = this.getHighLightString(line, config);
|
|
283
322
|
// 将高亮后的内容填充到 div 中
|
|
284
323
|
codeWrap.innerHTML = highlightedLine;
|
|
285
324
|
}
|
|
286
325
|
;
|
|
287
326
|
/**
|
|
288
|
-
*
|
|
289
|
-
* @param newCode -
|
|
327
|
+
* Highlights new source code and appends it to the code body.
|
|
328
|
+
* @param newCode - The new source code to highlight and append.
|
|
290
329
|
*/
|
|
291
330
|
async highlight(newCode) {
|
|
292
|
-
const config =
|
|
331
|
+
const config = Coax.languages.get(this.lang), startIndex = this.highlightedCodeEl.children.length,
|
|
293
332
|
// 将新源码按行分割
|
|
294
333
|
newCodeLines = newCode ? newCode.split('\n') : [];
|
|
295
334
|
//更新别名
|
|
@@ -304,7 +343,7 @@ class CoaxElem extends HTMLElement {
|
|
|
304
343
|
// 创建一个 div 包裹每一行
|
|
305
344
|
const lineWrap = this.createLineWrap(index, startIndex), codeWrap = lineWrap.lastElementChild;
|
|
306
345
|
//标记完成
|
|
307
|
-
|
|
346
|
+
lineWrap.completed = true;
|
|
308
347
|
if (this.hasAttribute('speed')) {
|
|
309
348
|
this.highlightedCodeEl.appendChild(lineWrap);
|
|
310
349
|
//流式打字
|
|
@@ -327,13 +366,19 @@ class CoaxElem extends HTMLElement {
|
|
|
327
366
|
this.autoScrollCode();
|
|
328
367
|
return true;
|
|
329
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Automatically scrolls the code body to the bottom.
|
|
371
|
+
*/
|
|
330
372
|
autoScrollCode() {
|
|
331
373
|
if (this.autoScroll) {
|
|
332
374
|
this.codeBodyEl.scrollTop = this.codeBodyEl.scrollHeight;
|
|
333
375
|
}
|
|
334
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Injects the theme styles for syntax highlighting (light/dark modes).
|
|
379
|
+
*/
|
|
335
380
|
injectThemeStyles() {
|
|
336
|
-
const config =
|
|
381
|
+
const config = Coax.languages.get(this.lang);
|
|
337
382
|
if (!config)
|
|
338
383
|
return;
|
|
339
384
|
// Get language name, fallback to 'js' if not provided
|
|
@@ -355,13 +400,16 @@ class CoaxElem extends HTMLElement {
|
|
|
355
400
|
.${NAMESPACE}-${cssPrefix}-${rule.token} { color: var(--${NAMESPACE}-${cssPrefix}-${rule.token},${rule.dark}); }` : ``} `).join('\n');
|
|
356
401
|
schemeStyles += `}`;
|
|
357
402
|
// Set the inner HTML of the shadow root, including styles and highlighted code
|
|
358
|
-
// 2. 精确更新 DOM 节点而非重写 ShadowRoot
|
|
359
403
|
this.dynamicStylesEl.textContent = lightStyles + darkStyles + schemeStyles;
|
|
360
404
|
//附加主题样式
|
|
361
405
|
if (config?.themeStyles) {
|
|
362
406
|
this.themeStylesEl.textContent = config.themeStyles;
|
|
363
407
|
}
|
|
364
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Updates the alias name for the language based on the configuration.
|
|
411
|
+
* @param config - The language configuration object.
|
|
412
|
+
*/
|
|
365
413
|
updateName(config) {
|
|
366
414
|
if (this.hasAttribute('unnamed'))
|
|
367
415
|
return;
|
|
@@ -375,11 +423,10 @@ class CoaxElem extends HTMLElement {
|
|
|
375
423
|
}
|
|
376
424
|
}
|
|
377
425
|
/**
|
|
378
|
-
|
|
379
|
-
|
|
426
|
+
* Renders the highlighted code within the shadow DOM.
|
|
427
|
+
*/
|
|
380
428
|
render(code = this.source) {
|
|
381
429
|
//同时多次改变属性,只执行一次
|
|
382
|
-
// 如果已经在队列中,则直接返回
|
|
383
430
|
if (this._renderQueued)
|
|
384
431
|
return;
|
|
385
432
|
this._renderQueued = true;
|
|
@@ -392,20 +439,23 @@ class CoaxElem extends HTMLElement {
|
|
|
392
439
|
this._renderQueued = false;
|
|
393
440
|
});
|
|
394
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Clears the current content and resets the state.
|
|
444
|
+
*/
|
|
395
445
|
clear() {
|
|
396
446
|
this.highlightedCodeEl.innerHTML = this.source = this.lineString = '';
|
|
397
447
|
}
|
|
398
448
|
/**
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
449
|
+
* Replaces the existing code with new source code and re-renders.
|
|
450
|
+
* @param newCode - The new source code to replace the existing code.
|
|
451
|
+
*/
|
|
402
452
|
async replace(newCode) {
|
|
403
453
|
this.clear();
|
|
404
454
|
await this.highlight(newCode);
|
|
405
455
|
}
|
|
406
456
|
/**
|
|
407
|
-
*
|
|
408
|
-
* @param newCode -
|
|
457
|
+
* Appends new source code to the current content and highlights only the new portion.
|
|
458
|
+
* @param newCode - The new source code to append and highlight.
|
|
409
459
|
*/
|
|
410
460
|
async append(newCode) {
|
|
411
461
|
// 将新的代码追加到现有代码末尾
|
|
@@ -413,7 +463,11 @@ class CoaxElem extends HTMLElement {
|
|
|
413
463
|
// 高亮新的部分
|
|
414
464
|
await this.highlight(newCode);
|
|
415
465
|
}
|
|
416
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Retrieves the last line of code that was rendered.
|
|
468
|
+
* @returns An object containing the line wrapper and code wrapper for the last line.
|
|
469
|
+
*/
|
|
470
|
+
getLastLine() {
|
|
417
471
|
const lastChild = this.highlightedCodeEl.lastElementChild, lastLine = !lastChild || this.highlightedCodeEl.lastElementChild?.completed ?
|
|
418
472
|
this.createLineWrap() : lastChild;
|
|
419
473
|
return {
|
|
@@ -421,28 +475,45 @@ class CoaxElem extends HTMLElement {
|
|
|
421
475
|
codeWrap: lastLine.lastElementChild
|
|
422
476
|
};
|
|
423
477
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
478
|
+
/**
|
|
479
|
+
* Marks the current line as completed and updates the displayed code.
|
|
480
|
+
*/
|
|
481
|
+
close() {
|
|
482
|
+
const lineWrap = this.highlightedCodeEl.lastElementChild;
|
|
483
|
+
if (!lineWrap)
|
|
484
|
+
return;
|
|
485
|
+
lineWrap.completed = true;
|
|
486
|
+
//行结束前保存
|
|
487
|
+
this.lastLineString = this.lineString;
|
|
488
|
+
this.getLineToFill(lineWrap?.lastElementChild, this.lineString);
|
|
489
|
+
//一行结束,清空临时行文本
|
|
490
|
+
this.lineString = '';
|
|
427
491
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
492
|
+
/**
|
|
493
|
+
* Reopens the last closed line and restores its original content.
|
|
494
|
+
*/
|
|
495
|
+
open() {
|
|
496
|
+
const lineWrap = this.highlightedCodeEl.lastElementChild;
|
|
497
|
+
if (!lineWrap)
|
|
498
|
+
return;
|
|
499
|
+
lineWrap.completed = false;
|
|
500
|
+
//恢复最后一行字符串
|
|
501
|
+
(lineWrap?.lastElementChild).textContent = this.lineString = this.lastLineString;
|
|
431
502
|
}
|
|
432
|
-
|
|
433
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Streams a string of code into the component, either appending or closing the current line.
|
|
505
|
+
* @param str - The code string to stream into the component.
|
|
506
|
+
* @param forceClose - Forcefully close the line if set to `true`.
|
|
507
|
+
*/
|
|
508
|
+
stream(str, forceClose = false) {
|
|
509
|
+
const { lineWrap, codeWrap } = this.getLastLine();
|
|
434
510
|
this.highlightedCodeEl.appendChild(lineWrap);
|
|
435
511
|
//汇集
|
|
436
512
|
this.source += str;
|
|
437
513
|
//如果没有遇到换行符号,也可以强制结束
|
|
438
|
-
|
|
439
|
-
if (str.startsWith('\n') || str.startsWith('\r')) {
|
|
514
|
+
if (forceClose || (str.startsWith('\n') || str.endsWith('\n'))) {
|
|
440
515
|
//标记完成
|
|
441
|
-
this.
|
|
442
|
-
//渲染
|
|
443
|
-
this.getLineToFill(codeWrap, this.lineString);
|
|
444
|
-
//一行结束,清空临时行文本
|
|
445
|
-
this.lineString = '';
|
|
516
|
+
this.close();
|
|
446
517
|
}
|
|
447
518
|
else {
|
|
448
519
|
//插入文本
|
|
@@ -454,4 +525,4 @@ class CoaxElem extends HTMLElement {
|
|
|
454
525
|
this.autoScrollCode();
|
|
455
526
|
}
|
|
456
527
|
}
|
|
457
|
-
export default
|
|
528
|
+
export default Coax;
|