@codady/coax 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/coax.cjs.js +421 -257
  2. package/dist/coax.cjs.min.js +3 -3
  3. package/dist/coax.esm.js +415 -257
  4. package/dist/coax.esm.min.js +3 -3
  5. package/dist/coax.umd.js +435 -249
  6. package/dist/coax.umd.min.js +3 -3
  7. package/examples/.htaccess +0 -0
  8. package/examples/append-highlight.html +82 -0
  9. package/examples/color-selector.html +412 -0
  10. package/examples/deepseek-highlight.html +91 -0
  11. package/examples/js-highlight.html +1 -1
  12. package/examples/md-highlight.html +60 -0
  13. package/examples/nginx.htaccess +0 -0
  14. package/examples/replace-highlight.html +69 -0
  15. package/examples/stream-highlight.html +64 -0
  16. package/examples/theme-highlight.html +69 -0
  17. package/package.json +3 -3
  18. package/rollup.config.js +3 -3
  19. package/src/Coax.js +26 -414
  20. package/src/Coax.ts +29 -443
  21. package/src/components/CoaxElem.js +457 -0
  22. package/src/components/CoaxElem.ts +488 -0
  23. package/src/modules.js +12 -0
  24. package/src/modules.ts +23 -0
  25. package/src/rules/css.js +11 -0
  26. package/src/rules/css.ts +11 -0
  27. package/src/rules/html.js +13 -0
  28. package/src/rules/html.ts +13 -0
  29. package/src/rules/javascript.js +10 -0
  30. package/src/rules/javascript.ts +10 -0
  31. package/src/rules/markdown.js +29 -0
  32. package/src/rules/markdown.ts +41 -0
  33. package/src/rules/ruleCss - /345/211/257/346/234/254.js" +10 -0
  34. package/src/rules/ruleHTML - /345/211/257/346/234/254.js" +12 -0
  35. package/src/rules/ruleJs - /345/211/257/346/234/254.js" +10 -0
  36. package/src/rules/ruleTs - /345/211/257/346/234/254.js" +12 -0
  37. package/src/rules/typescript.js +12 -0
  38. package/src/rules/typescript.ts +12 -0
  39. package/src/tools/copy.js +26 -0
  40. package/src/tools/copy.ts +29 -0
package/dist/coax.cjs.js CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
- * @since Last modified: 2026-1-8 16:55:44
3
+ * @since Last modified: 2026-1-12 9:47:5
4
4
  * @name Coax event management system.
5
- * @version 0.0.2
5
+ * @version 0.0.3
6
6
  * @author AXUI development team <3217728223@qq.com>
7
7
  * @description Coax is a custom web component that enables syntax highlighting for various programming languages inside your HTML documents.
8
8
  * @see {@link https://coax.axui.cn|Official website}
@@ -16,6 +16,75 @@
16
16
 
17
17
  'use strict';
18
18
 
19
+ const typeWriter = (text, options) => {
20
+ const speed = options.speed || 100; // Set typing speed (default to 100ms per character)
21
+ return new Promise((resolve) => {
22
+ // Callback before typing starts
23
+ options?.onBeforeType?.(text);
24
+ let index = 0;
25
+ // Timer to type the text character by character at the given speed
26
+ const timer = setInterval(() => {
27
+ if (index < text.length) {
28
+ const char = text.charAt(index); // Get the character at the current index
29
+ const typedText = text.substring(0, index + 1); // The text typed so far
30
+ // Callback during typing each character
31
+ options?.onDuringType?.(char, typedText);
32
+ index++;
33
+ }
34
+ else {
35
+ // Clear the timer once typing is complete
36
+ clearInterval(timer);
37
+ // Resolve the Promise when typing is complete
38
+ resolve(text);
39
+ // Callback after typing is finished
40
+ options?.onAfterType?.(text);
41
+ }
42
+ }, speed);
43
+ });
44
+ };
45
+
46
+ const COMMA$1 = ',';
47
+
48
+ const SPACE$1 = ' ';
49
+
50
+ const trim$1 = (str, placement = 'compress') => {
51
+ if (typeof str !== 'string') {
52
+ console.warn('Expected a string input');
53
+ return '';
54
+ }
55
+ switch (placement) {
56
+ case 'start':
57
+ return str.trimStart();
58
+ case 'end':
59
+ return str.trimEnd();
60
+ case 'both':
61
+ return str.trim();
62
+ case 'global':
63
+ return str.replace(/[\s\r\n]+/g, '');
64
+ default:
65
+ return str.trim().replace(/[\s\r\n]+/g, ' '); // Default behavior, trims both ends and replaces inner spaces
66
+ }
67
+ };
68
+
69
+ const parseClasses$1 = (data) => {
70
+ let separator, result = [];
71
+ if (Array.isArray(data)) {
72
+ // If data is already an array, filter out invalid values
73
+ result = data.filter((k) => k && typeof k === 'string');
74
+ }
75
+ else {
76
+ // Trim the input string and handle multiple spaces
77
+ data = trim$1(data);
78
+ // Use comma as the separator if present, otherwise use space
79
+ separator = data.includes(COMMA$1) ? COMMA$1 : SPACE$1;
80
+ result = data.split(separator);
81
+ }
82
+ // Trim each item globally and filter out any empty strings
83
+ return result.map((k) => trim$1(k, 'global')).filter(Boolean);
84
+ };
85
+
86
+ const NAMESPACE = 'ax';
87
+
19
88
  const getDataType = (obj) => {
20
89
  let tmp = Object.prototype.toString.call(obj).slice(8, -1), result;
21
90
  if (tmp === 'Function' && /^\s*class\s+/.test(obj.toString())) {
@@ -158,48 +227,6 @@ const isEmpty = (data) => {
158
227
 
159
228
  const ALIAS = 'rep';
160
229
 
161
- const NAMESPACE = 'ax';
162
-
163
- const COMMA$1 = ',';
164
-
165
- const SPACE$1 = ' ';
166
-
167
- const trim$1 = (str, placement = '') => {
168
- if (typeof str !== 'string') {
169
- console.warn('Expected a string input');
170
- return '';
171
- }
172
- switch (placement) {
173
- case 'start':
174
- return str.trimStart();
175
- case 'end':
176
- return str.trimEnd();
177
- case 'both':
178
- return str.trim();
179
- case 'global':
180
- return str.replace(/[\s\r\n]+/g, '');
181
- default:
182
- return str.trim().replace(/[\s\r\n]+/g, ' '); // Default behavior, trims both ends and replaces inner spaces
183
- }
184
- };
185
-
186
- const parseClasses$1 = (data) => {
187
- let separator, result = [];
188
- if (Array.isArray(data)) {
189
- // If data is already an array, filter out invalid values
190
- result = data.filter((k) => k && typeof k === 'string');
191
- }
192
- else {
193
- // Trim the input string and handle multiple spaces
194
- data = trim$1(data);
195
- // Use comma as the separator if present, otherwise use space
196
- separator = data.includes(COMMA$1) ? COMMA$1 : SPACE$1;
197
- result = data.split(separator);
198
- }
199
- // Trim each item globally and filter out any empty strings
200
- return result.map((k) => trim$1(k, 'global')).filter(Boolean);
201
- };
202
-
203
230
  const addClasses = (target, classes, intercept) => {
204
231
  const el = getEl(target), arr = parseClasses$1(classes);
205
232
  if (!el || arr.length === 0) {
@@ -241,72 +268,7 @@ const createTools = (data) => {
241
268
  return toolsEl;
242
269
  };
243
270
 
244
- const COMMA = ',';
245
-
246
- const SPACE = ' ';
247
-
248
- const trim = (str, placement = '') => {
249
- if (typeof str !== 'string') {
250
- console.warn('Expected a string input');
251
- return '';
252
- }
253
- switch (placement) {
254
- case 'start':
255
- return str.trimStart();
256
- case 'end':
257
- return str.trimEnd();
258
- case 'both':
259
- return str.trim();
260
- case 'global':
261
- return str.replace(/[\s\r\n]+/g, '');
262
- default:
263
- return str.trim().replace(/[\s\r\n]+/g, ' '); // Default behavior, trims both ends and replaces inner spaces
264
- }
265
- };
266
-
267
- const parseClasses = (data) => {
268
- let separator, result = [];
269
- if (Array.isArray(data)) {
270
- // If data is already an array, filter out invalid values
271
- result = data.filter((k) => k && typeof k === 'string');
272
- }
273
- else {
274
- // Trim the input string and handle multiple spaces
275
- data = trim(data);
276
- // Use comma as the separator if present, otherwise use space
277
- separator = data.includes(COMMA) ? COMMA : SPACE;
278
- result = data.split(separator);
279
- }
280
- // Trim each item globally and filter out any empty strings
281
- return result.map((k) => trim(k, 'global')).filter(Boolean);
282
- };
283
-
284
- const rtlStyle = (name = '') => `
285
- <style>
286
- :where([dir="rtl"]) .icax-${name},
287
- :where(:dir(rtl)) .icax-${name} {
288
- transform: scaleX(-1);
289
- transform-origin: center;
290
- }
291
- </style>
292
- `;
293
-
294
- const wrap = (content, fun, isRtl = false, options) => {
295
- const size = options?.size || '1em', color = options?.color || 'currentColor', thickness = options?.thickness || 2, classes = options?.classes ? parseClasses(options.classes).join(' ') : '',
296
- // 得到 "icax-left"
297
- origName = fun.name.replace(/([A-Z])/g, "-$1").toLowerCase();
298
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}"
299
- stroke-width="${thickness}" stroke-linecap="round" stroke-linejoin="round" class="${origName} ${classes}">
300
- ${isRtl ? rtlStyle(origName.split('-')[1]) : ''}
301
- ${content}
302
- </svg>`;
303
- };
304
-
305
- const icaxCopy = (options) => wrap(`<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>`, icaxCopy, false, options);
306
-
307
- const icaxCheck = (options) => wrap(`<polyline points="20 6 9 17 4 12"></polyline>`, icaxCheck, false, options);
308
-
309
- class Coax extends HTMLElement {
271
+ class CoaxElem extends HTMLElement {
310
272
  // A static Map to hold the configuration for different languages
311
273
  static languages = new Map();
312
274
  static tools = [];
@@ -317,9 +279,13 @@ class Coax extends HTMLElement {
317
279
  dynamicStylesEl;
318
280
  highlightedCodeEl;
319
281
  headerEl;
320
- nameEl;
321
- toolsEl;
282
+ codeNameEl;
283
+ codeToolsEl;
284
+ codeBodyEl;
322
285
  alias;
286
+ lineString;
287
+ speed;
288
+ autoScroll;
323
289
  constructor() {
324
290
  super();
325
291
  // Attach a Shadow DOM to the custom element
@@ -328,6 +294,9 @@ class Coax extends HTMLElement {
328
294
  this.source = this.textContent?.replace(/^\s*\n|\n\s*$/g, '') || '';
329
295
  this.lang = 'plain';
330
296
  this.alias = 'Plain Text';
297
+ this.lineString = '';
298
+ this.speed = 5;
299
+ this.autoScroll = true;
331
300
  // 1. 初始化基础骨架
332
301
  this.shadowRoot.innerHTML = `
333
302
  <style id="base-styles">
@@ -345,16 +314,16 @@ class Coax extends HTMLElement {
345
314
  --border-color:rgb(224, 224, 224);
346
315
  --color-code:rgb(51, 51, 51);
347
316
  --color-index:rgb(153, 153, 153);
348
- --color-stripe:rgb(224, 224, 224);
349
- --color-hover:rgb(224, 224, 224);
317
+ --color-stripe:rgba(0,0,0,0.04);
318
+ --color-hover:rgba(0,0,0,0.06);
350
319
  }
351
320
  :host([scheme="dark"]){
352
321
  --background: #282c34;
353
322
  --border-color: transparent;
354
323
  --color-code: #abb2bf;
355
324
  --color-index:rgb(153, 153, 153);
356
- --color-stripe:rgb(66, 66, 66);
357
- --color-hover:rgb(66, 66, 66);
325
+ --color-stripe:rgba(255,255,255,0.04);
326
+ --color-hover:rgba(255,255,255,0.06);
358
327
  }
359
328
  @media (prefers-color-scheme: dark) {
360
329
  :host{
@@ -362,8 +331,8 @@ class Coax extends HTMLElement {
362
331
  --border-color: transparent;
363
332
  --color-code: #abb2bf;
364
333
  --color-index:rgb(153, 153, 153);
365
- --color-stripe:rgb(66, 66, 66);
366
- --color-hover:rgb(66, 66, 66);
334
+ --color-stripe:rgba(255,255,255,0.04);
335
+ --color-hover:rgba(255,255,255,0.06);
367
336
  }
368
337
  }
369
338
  :host {
@@ -373,7 +342,6 @@ class Coax extends HTMLElement {
373
342
  background:var(--${NAMESPACE}-code-background-color,var(--background));
374
343
  color:var(--${NAMESPACE}-code-color,var(--color-code));
375
344
  border:var(--${NAMESPACE}-code-border-width,var(--border-width)) var(--${NAMESPACE}-code-border-style,var(--border-style)) var(--${NAMESPACE}-code-border-color,var(--border-color));
376
- overflow:auto;
377
345
  transition: border-color 0.3s ease,color 0.3s ease;
378
346
  border-radius: var(--${NAMESPACE}-code-radius,var(--radius));
379
347
  }
@@ -390,6 +358,7 @@ class Coax extends HTMLElement {
390
358
  padding: var(--${NAMESPACE}-code-padding,var(--padding)) 0;
391
359
  height:var(--${NAMESPACE}-code-height,var(--height));
392
360
  max-height:var(--${NAMESPACE}-code-max-height,var(--max-height));
361
+ overflow:auto;
393
362
  }
394
363
  pre,code{
395
364
  font-family:"Consolas", "Monaco", "Andale Mono", "Ubuntu Mono", "monospace";
@@ -405,6 +374,9 @@ class Coax extends HTMLElement {
405
374
  flex:auto;
406
375
  }
407
376
  }
377
+ code>div>div:empty:before{
378
+ content:' ';
379
+ }
408
380
  :host([indexed]) code>div:before{
409
381
  display:inline-flex;
410
382
  color:var(--color-index);
@@ -412,13 +384,13 @@ class Coax extends HTMLElement {
412
384
  min-width:var(--${NAMESPACE}-code-index-width,2em);
413
385
  margin-inline-end:var(--${NAMESPACE}-code-padding,8px);
414
386
  }
415
- :host([hoverable]) code>div:hover{
416
- background-color:var(--color-hover);
417
- }
418
387
  :host([striped]) code>div:nth-child(odd){
419
388
  background-color:var(--color-stripe);
420
389
  }
421
- :host([wrapped]) code>div{
390
+ :host([hoverable]) code>div:hover{
391
+ background-color:var(--color-hover);
392
+ }
393
+ :host([wrapped]) code>div>div{
422
394
  white-space: pre-wrap;
423
395
  }
424
396
  :host([unnamed]) #code-name{
@@ -463,9 +435,15 @@ class Coax extends HTMLElement {
463
435
  this.themeStylesEl = getEl('#theme-styles', this.shadowRoot);
464
436
  this.dynamicStylesEl = getEl('#dynamic-styles', this.shadowRoot);
465
437
  this.headerEl = getEl('#code-header', this.shadowRoot);
466
- this.nameEl = getEl('#code-name', this.shadowRoot);
467
- this.toolsEl = getEl('#code-tools', this.shadowRoot);
438
+ this.codeNameEl = getEl('#code-name', this.shadowRoot);
439
+ this.codeToolsEl = getEl('#code-tools', this.shadowRoot);
440
+ this.codeBodyEl = getEl('#code-body', this.shadowRoot);
468
441
  this.highlightedCodeEl = getEl('#highlight-code', this.shadowRoot);
442
+ this.codeBodyEl.addEventListener('scroll', () => {
443
+ let flag = this.codeBodyEl.scrollTop + this.codeBodyEl.clientHeight < this.codeBodyEl.scrollHeight;
444
+ // 检测是否是用户手动滚动
445
+ this.autoScroll = !flag;
446
+ });
469
447
  }
470
448
 
471
449
  static register(name, config) {
@@ -474,17 +452,17 @@ class Coax extends HTMLElement {
474
452
  }
475
453
  //注册工具箱
476
454
  static addTools(items) {
477
- Coax.tools = items;
455
+ CoaxElem.tools = items;
478
456
  }
479
457
  //加入工具箱
480
458
  mountTools(toolItems) {
481
459
  requestAnimationFrame(() => {
482
- this.toolsEl.innerHTML = '';
460
+ this.codeToolsEl.innerHTML = '';
483
461
  let items = toolItems.map(k => {
484
462
  k.action = k.action.bind(this);
485
463
  return k;
486
464
  });
487
- this.toolsEl.appendChild(createTools(items));
465
+ this.codeToolsEl.appendChild(createTools(items));
488
466
  });
489
467
  }
490
468
  // Called when the element is connected to the DOM
@@ -492,7 +470,7 @@ class Coax extends HTMLElement {
492
470
  this.render();
493
471
  }
494
472
  // Observed attributes for changes
495
- static get observedAttributes() { return ['lang', 'height', 'max-height', 'tools']; }
473
+ static get observedAttributes() { return ['lang', 'height', 'max-height', 'tools', 'speed']; }
496
474
 
497
475
  attributeChangedCallback(name, oldVal, newVal) {
498
476
  if (oldVal === newVal)
@@ -500,19 +478,18 @@ class Coax extends HTMLElement {
500
478
  if (name === 'height' || name === 'max-height') {
501
479
  this.updateStyleByRegExp(name, newVal);
502
480
  }
503
- else if (name === 'lang') {
481
+ if (name === 'speed') {
482
+ this.speed = ~~(!!newVal);
483
+ }
484
+ if (name === 'lang') {
504
485
  //更新当前语言
505
486
  this.lang = newVal;
506
487
  this.render();
507
488
  }
508
- else if (name === 'unnamed') {
509
- this.nameEl.innerHTML = newVal === null ? (this.alias || this.lang) : '';
510
- this.nameEl.innerHTML = '';
511
- }
512
- else if (name === 'tools') {
489
+ if (name === 'tools') {
513
490
  if (!newVal)
514
- this.toolsEl.innerHTML = '';
515
- const tools = parseClasses$1(newVal), toolItems = Coax.tools.filter(k => tools.includes(k.name));
491
+ this.codeToolsEl.innerHTML = '';
492
+ const tools = parseClasses$1(newVal), toolItems = CoaxElem.tools.filter(k => tools.includes(k.name));
516
493
  if (!toolItems.length)
517
494
  return;
518
495
  this.mountTools(toolItems);
@@ -529,36 +506,84 @@ class Coax extends HTMLElement {
529
506
  getCssPrefix(config) {
530
507
  return (config?.cssPrefix || this.lang).replace(/[^a-zA-Z0-9_-]/g, '\\$&');
531
508
  }
509
+ getHighLightString(string, config) {
510
+ config = config || CoaxElem.languages.get(this.lang);
511
+ if (!config)
512
+ return string;
513
+ // 如果找到了配置,则进行高亮处理
514
+ const cssPrefix = this.getCssPrefix(config),
515
+ // 获取用于语法高亮的正则表达式
516
+ combinedRegex = new RegExp(config.rules.map((r) => `(${r.pattern.source})`).join('|'), 'g');
517
+ return string.replace(combinedRegex, (match, ...args) => {
518
+ const i = args.findIndex(val => val !== undefined);
519
+ return i !== -1 && config.rules[i] ? `<span class="${NAMESPACE}-${cssPrefix}-${config.rules[i].token}">${match}</span>` : match;
520
+ });
521
+ }
522
+ createLineWrap(index, startIndex) {
523
+ let dataIndex = 0;
524
+ if (index == null && startIndex == null) {
525
+ dataIndex = this.highlightedCodeEl.children.length;
526
+ }
527
+ else {
528
+ const start = startIndex || this.highlightedCodeEl.children.length;
529
+ dataIndex = start + index;
530
+ }
531
+ return createEl('div', { 'data-index': dataIndex }, '<div></div>');
532
+ }
533
+ getLineToFill(codeWrap, line, config) {
534
+ config = config || CoaxElem.languages.get(this.lang);
535
+ let highlightedLine = this.getHighLightString(line, config);
536
+ // 将高亮后的内容填充到 div 中
537
+ codeWrap.innerHTML = highlightedLine;
538
+ }
539
+ ;
532
540
 
533
- highlight(newCode) {
534
- const config = Coax.languages.get(this.lang), startIndex = this.highlightedCodeEl.children.length,
541
+ async highlight(newCode) {
542
+ const config = CoaxElem.languages.get(this.lang), startIndex = this.highlightedCodeEl.children.length,
535
543
  // 将新源码按行分割
536
- newCodeLines = newCode.split('\n'),
544
+ newCodeLines = newCode ? newCode.split('\n') : [];
545
+ //更新别名
546
+ this.updateName(config);
547
+ if (!newCodeLines.length)
548
+ return true;
537
549
  // 如果没有找到配置,则输出原始代码,并不进行高亮处理
538
- highlightedLines = newCodeLines.map((line, index) => {
539
- let highlightedLine = line;
540
- // 如果找到了配置,则进行高亮处理
541
- if (config) {
542
- const cssPrefix = this.getCssPrefix(config);
543
- // 获取用于语法高亮的正则表达式
544
- const combinedRegex = new RegExp(config.rules.map((r) => `(${r.pattern.source})`).join('|'), 'g');
545
- // 进行高亮替换
546
- highlightedLine = line.replace(combinedRegex, (match, ...args) => {
547
- const i = args.findIndex(val => val !== undefined);
548
- return i !== -1 && config.rules[i] ? `<span class="${NAMESPACE}-${cssPrefix}-${config.rules[i].token}">${match}</span>` : match;
550
+ for (let [index, lineString] of newCodeLines.entries()) {
551
+ //如果是空行则跳过
552
+ if (!lineString.trim() && this.hasAttribute('sanitized'))
553
+ continue;
554
+ // 创建一个 div 包裹每一行
555
+ const lineWrap = this.createLineWrap(index, startIndex), codeWrap = lineWrap.lastElementChild;
556
+ //标记完成
557
+ this.closeLine(lineWrap);
558
+ if (this.hasAttribute('speed')) {
559
+ this.highlightedCodeEl.appendChild(lineWrap);
560
+ //流式打字
561
+ await typeWriter(lineString, {
562
+ speed: this.speed,
563
+ onDuringType: (char, fullText) => {
564
+ codeWrap.innerHTML = fullText;
565
+ }
549
566
  });
567
+ this.getLineToFill(codeWrap, lineString, config);
550
568
  }
551
- // 创建一个 div 包裹每一行
552
- const lineDiv = createEl('div', { 'data-index': startIndex + index });
553
- lineDiv.innerHTML = `<div>${highlightedLine}</div>`; // 将高亮后的内容填充到 div 中
554
- return lineDiv;
555
- });
556
- //
557
- // 将新生成的行节点追加到 highlightedCodeEl 节点中
558
- this.highlightedCodeEl.append(...highlightedLines);
569
+ else {
570
+ //直接打出
571
+ this.getLineToFill(codeWrap, lineString, config);
572
+ //
573
+ this.highlightedCodeEl.appendChild(lineWrap);
574
+ }
575
+ }
576
+ //滚动到最底部
577
+ this.autoScrollCode();
578
+ return true;
579
+ }
580
+ autoScrollCode() {
581
+ if (this.autoScroll) {
582
+ this.codeBodyEl.scrollTop = this.codeBodyEl.scrollHeight;
583
+ }
559
584
  }
560
585
  injectThemeStyles() {
561
- const config = Coax.languages.get(this.lang);
586
+ const config = CoaxElem.languages.get(this.lang);
562
587
  if (!config)
563
588
  return;
564
589
  // Get language name, fallback to 'js' if not provided
@@ -566,17 +591,17 @@ class Coax extends HTMLElement {
566
591
  //Generate dynamic CSS classes for each syntax rule
567
592
  // 为 rules 中的每一个 name 生成类似 .hl-name { color: var(--ax-code-name); }
568
593
  lightStyles = config.rules.map((rule) => `
569
- .${NAMESPACE}-${cssPrefix}-${rule.token} { color: var(--${NAMESPACE}-${cssPrefix}-${rule.token}${rule.color ? ',' + rule.color : ''});}`).join('\n'), darkStyles = '', schemeStyles = '';
594
+ .${NAMESPACE}-${cssPrefix}-${rule.token} { color: var(--${NAMESPACE}-${cssPrefix}-${rule.token}${rule.light ? ',' + rule.light : ''});}`).join('\n'), darkStyles = '', schemeStyles = '';
570
595
  darkStyles += `:host([scheme="dark"]){`;
571
596
  darkStyles += config.rules.map((rule) => `
572
- ${rule.color ? `
597
+ ${rule.light ? `
573
598
  .${NAMESPACE}-${cssPrefix}-${rule.token} {color: var(--${NAMESPACE}-${cssPrefix}-${rule.token},${rule.dark});}` : ``}`).join('\n');
574
599
  darkStyles += `}`;
575
600
  schemeStyles = `@media (prefers-color-scheme: dark){
576
601
  :host{
577
602
  `;
578
603
  schemeStyles += config.rules.map((rule) => `
579
- ${rule.color ? `
604
+ ${rule.light ? `
580
605
  .${NAMESPACE}-${cssPrefix}-${rule.token} { color: var(--${NAMESPACE}-${cssPrefix}-${rule.token},${rule.dark}); }` : ``} `).join('\n');
581
606
  schemeStyles += `}`;
582
607
  // Set the inner HTML of the shadow root, including styles and highlighted code
@@ -586,121 +611,260 @@ class Coax extends HTMLElement {
586
611
  if (config?.themeStyles) {
587
612
  this.themeStylesEl.textContent = config.themeStyles;
588
613
  }
589
- //更新别名
590
- this.updateName(config);
591
614
  }
592
615
  updateName(config) {
593
616
  if (this.hasAttribute('unnamed'))
594
617
  return;
595
- //更新别名
596
- this.alias = config.alias || this.lang;
597
- this.nameEl.innerHTML = this.alias;
618
+ if (!config) {
619
+ this.codeNameEl.innerHTML = 'Plain Text';
620
+ }
621
+ else {
622
+ //更新别名
623
+ this.alias = config.alias || this.lang;
624
+ this.codeNameEl.innerHTML = this.alias;
625
+ }
598
626
  }
599
627
 
600
- render() {
628
+ render(code = this.source) {
601
629
  //同时多次改变属性,只执行一次
602
630
  // 如果已经在队列中,则直接返回
603
631
  if (this._renderQueued)
604
632
  return;
605
633
  this._renderQueued = true;
606
634
  // 使用 requestAnimationFrame 将渲染推迟到下一帧
607
- requestAnimationFrame(() => {
608
- this.highlightedCodeEl.innerHTML = '';
609
- //一次性渲染
610
- this.highlight(this.source);
635
+ requestAnimationFrame(async () => {
636
+ this.clear();
611
637
  this.injectThemeStyles();
638
+ //一次性渲染
639
+ await this.highlight(code);
612
640
  this._renderQueued = false;
613
641
  });
614
642
  }
643
+ clear() {
644
+ this.highlightedCodeEl.innerHTML = this.source = this.lineString = '';
645
+ }
615
646
 
616
- replaceCode(newCode) {
617
- // 更新原始代码为新代码
618
- this.source = newCode;
619
- //清空
620
- this.highlightedCodeEl.innerHTML = '';
621
- this.highlight(newCode);
647
+ async replace(newCode) {
648
+ this.clear();
649
+ await this.highlight(newCode);
622
650
  }
623
651
 
624
- appendCode(newCode) {
652
+ async append(newCode) {
625
653
  // 将新的代码追加到现有代码末尾
626
654
  this.source += `\n${newCode}`;
627
655
  // 高亮新的部分
628
- this.highlight(newCode);
656
+ await this.highlight(newCode);
629
657
  }
630
- }
631
- Coax.register('css', {
632
- rules: [
633
- { token: 'comment', pattern: /\/\*[\s\S]*?\*\//, color: '#6a737d', dark: '#8b949e' },
634
- { token: 'value', pattern: /(?:'|")(?:\\.|[^\\'"\b])*?(?:'|")/, color: '#61afef', dark: '#a5d6ff' },
635
- { token: 'func', pattern: /[a-z-]+\(?=/, color: '#e36209', dark: '#ffa657' },
636
- { token: 'property', pattern: /[a-z-]+(?=\s*:)/, color: '#005cc5', dark: '#79c0ff' },
637
- { token: 'selector', pattern: /[.#a-z0-9, \n\t>:+()_-]+(?=\s*\{)/i, color: '#6f42c1', dark: '#d2a8ff' },
638
- { token: 'unit', pattern: /(?<=\d)(px|em|rem|%|vh|vw|ms|s|deg)/, color: '#d73a49', dark: '#ff7b72' },
639
- { token: 'number', pattern: /\b\d+(\.\d+)?\b/, color: '#005cc5', dark: '#79c0ff' },
640
- { token: 'punct', pattern: /[{}();:]/, color: '#24292e', dark: '#c9d1d9' }
641
- ],
642
-
643
- });
644
- Coax.register('html', {
645
- rules: [
646
- { token: 'comment', pattern: /&lt;!--[\s\S]*?--&gt;/, color: '#6a737d', dark: '#8b949e' },
647
- { token: 'doctype', pattern: /&lt;!DOCTYPE[\s\S]*?&gt;/i, color: '#005cc5', dark: '#56b6c2' },
648
- // 匹配标签名: <div, </div
649
- { token: 'tag', pattern: /&lt;\/?[a-zA-Z0-9]+/, color: '#22863a', dark: '#abb2bf' },
650
- // 匹配属性名: class=
651
- { token: 'attr', pattern: /[a-zA-Z-]+(?=\s*=\s*)/, color: '#6f42c1', dark: '#e06c75' },
652
- // 匹配属性值: "value"
653
- { token: 'string', pattern: /(['"])(?:\\.|[^\\])*?\1/, color: '#032f62', dark: '#f39c12' },
654
- // 匹配标签闭合: >, />
655
- { token: 'bracket', pattern: /\/?&gt;/, color: '#24292e', dark: '#f1f1f1' }
656
- ],
657
- });
658
- Coax.register('js', {
659
- alias: 'Javascript',
660
- rules: [
661
- { token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, color: '#6a737d', dark: '#8b949e' },
662
- { token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, color: '#032f62', dark: '#61afef' },
663
- { token: 'keyword', pattern: /\b(async|await|break|case|catch|class|const|continue|default|delete|do|else|export|extends|finally|for|function|if|import|in|instanceof|new|return|super|switch|this|throw|try|typeof|var|while|with|yield|let|static)\b/, color: '#e06c75', dark: '#d73a49' },
664
- { token: 'builtin', pattern: /\b(console|window|document|Math|JSON|true|false|null|undefined|Object|Array|Promise)\b/, color: '#56b6c2', dark: '#61afef' },
665
- { token: 'number', pattern: /\b\d+\b/, color: '#61afef', dark: '#d19a66' },
666
- { token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, color: '#e5c07b', dark: '#98c379' },
667
- { token: 'op', pattern: /[+\-*/%=<>!&|^~]+/, color: '#d73a49', dark: '#e06c75' }
668
- ],
669
- });
670
- Coax.register('ts', {
671
- rules: [
672
- { token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, color: '#6a737d', dark: '#8b949e' },
673
- { token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, color: '#032f62', dark: '#61afef' },
674
- { token: 'decorator', pattern: /@[a-zA-Z_]\w*/, color: '#d19a66', dark: '#e5c07b' },
675
- { token: 'keyword', pattern: /\b(abstract|as|async|await|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|is|keyof|let|module|namespace|new|package|private|protected|public|readonly|return|set|static|super|switch|this|throw|try|type|typeof|var|while|with|yield)\b/, color: '#e06c75', dark: '#d73a49' },
676
- { token: 'builtin', pattern: /\b(any|boolean|never|number|string|symbol|unknown|void|undefined|null|boolean|true|false|console|window|document)\b/, color: '#56b6c2', dark: '#61afef' },
677
- { token: 'type', pattern: /\b[A-Z]\w*\b/, color: '#22863a', dark: '#8b949e' },
678
- { token: 'number', pattern: /\b\d+\b/, color: '#61afef', dark: '#d19a66' },
679
- { token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, color: '#e5c07b', dark: '#98c379' },
680
- { token: 'op', pattern: /(\?\.|![:\.]|[+\-*/%=<>!&|^~]+)/, color: '#d73a49', dark: '#e06c75' }
681
- ],
682
- });
683
- Coax.addTools([{
684
- name: 'copy',
685
- icon: icaxCopy(),
686
- action: function (arg) {
687
- arg.wrapEl.onclick = () => {
688
- //this只是组件实例
689
- navigator.clipboard.writeText(this.source)
690
- .then(() => {
691
- console.log('Text successfully copied to clipboard');
692
- arg.iconEl.innerHTML = icaxCheck();
693
- arg.iconEl.toggleAttribute('disabled', true);
694
- setTimeout(() => {
695
- //恢复
696
- arg.iconEl.removeAttribute('disabled');
697
- arg.iconEl.innerHTML = icaxCopy();
698
- }, 2000);
699
- })
700
- .catch(err => {
701
- console.error('Error copying text to clipboard: ', err);
702
- });
703
- };
658
+ getActiveCodeWrap() {
659
+ const lastChild = this.highlightedCodeEl.lastElementChild, lastLine = !lastChild || this.highlightedCodeEl.lastElementChild?.completed ?
660
+ this.createLineWrap() : lastChild;
661
+ return {
662
+ lineWrap: lastLine,
663
+ codeWrap: lastLine.lastElementChild
664
+ };
665
+ }
666
+ closeLine(lineWrap) {
667
+ lineWrap = lineWrap || this.highlightedCodeEl.lastElementChild;
668
+ lineWrap && (lineWrap.completed = true);
669
+ }
670
+ openLine(lineWrap) {
671
+ lineWrap = lineWrap || this.highlightedCodeEl.lastElementChild;
672
+ lineWrap && (lineWrap.completed = false);
673
+ }
674
+ stream(str, forceEnd = false) {
675
+ const { lineWrap, codeWrap } = this.getActiveCodeWrap();
676
+ this.highlightedCodeEl.appendChild(lineWrap);
677
+ //汇集
678
+ this.source += str;
679
+ //如果没有遇到换行符号,也可以强制结束
680
+ forceEnd && this.closeLine(lineWrap);
681
+ if (str.startsWith('\n') || str.startsWith('\r')) {
682
+ //标记完成
683
+ this.closeLine(lineWrap);
684
+ //渲染
685
+ this.getLineToFill(codeWrap, this.lineString);
686
+ //一行结束,清空临时行文本
687
+ this.lineString = '';
688
+ }
689
+ else {
690
+ //插入文本
691
+ codeWrap.innerHTML += str;
692
+ //临时保存行文本
693
+ this.lineString += str;
704
694
  }
705
- }]);
706
- customElements.define(`${NAMESPACE}-code`, Coax);
695
+ //滚动到最底部
696
+ this.autoScrollCode();
697
+ }
698
+ }
699
+
700
+ const css = [
701
+ { token: 'comment', pattern: /\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
702
+ { token: 'value', pattern: /(?:'|")(?:\\.|[^\\'"\b])*?(?:'|")/, light: '#032f62', dark: '#a5d6ff' },
703
+ { token: 'func', pattern: /[a-z-]+\(?=/, light: '#e36209', dark: '#ffa657' },
704
+ { token: 'property', pattern: /[a-z-]+(?=\s*:)/, light: '#005cc5', dark: '#79c0ff' },
705
+ { token: 'selector', pattern: /[.#a-z0-9, \n\t>:+()_-]+(?=\s*\{)/i, light: '#22863a', dark: '#7ee787' },
706
+ { token: 'unit', pattern: /(?<=\d)(px|em|rem|%|vh|vw|ms|s|deg)/, light: '#d73a49', dark: '#ff7b72' },
707
+ { token: 'number', pattern: /\b\d+(\.\d+)?\b/, light: '#005cc5', dark: '#79c0ff' },
708
+ { token: 'punct', pattern: /[{}();:]/, light: '#24292e', dark: '#c9d1d9' }
709
+ ];
710
+
711
+ const html = [
712
+ { token: 'comment', pattern: /&lt;!--[\s\S]*?--&gt;/, light: '#999999', dark: '#6e7681' },
713
+ { token: 'doctype', pattern: /&lt;!DOCTYPE[\s\S]*?&gt;/i, light: '#6a737d', dark: '#8b949e' },
714
+ // 匹配标签名: <div, </div
715
+ { token: 'tag', pattern: /&lt;\/?[a-zA-Z0-9]+/, light: '#22863a', dark: '#7ee787' },
716
+ // 匹配属性名: class=
717
+ { token: 'attr', pattern: /[a-zA-Z-]+(?=\s*=\s*)/, light: '#6f42c1', dark: '#d2a8ff' },
718
+ // 匹配属性值: "value"
719
+ { token: 'string', pattern: /(['"])(?:\\.|[^\\])*?\1/, light: '#032f62', dark: '#a5d6ff' },
720
+ // 匹配标签闭合: >, />
721
+ { token: 'bracket', pattern: /\/?&gt;/, light: '#24292e', dark: '#c9d1d9' }
722
+ ];
723
+
724
+ const javascript = [
725
+ { token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
726
+ { token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, light: '#032f62', dark: '#98c379' },
727
+ { token: 'keyword', pattern: /\b(async|await|break|case|catch|class|const|continue|default|delete|do|else|export|extends|finally|for|function|if|import|in|instanceof|new|return|super|switch|this|throw|try|typeof|var|while|with|yield|let|static)\b/, light: '#d73a49', dark: '#ff7b72' },
728
+ { token: 'builtin', pattern: /\b(console|window|document|Math|JSON|true|false|null|undefined|Object|Array|Promise|Number|String|Boolean)\b/, light: '#e36209', dark: '#ffa657' },
729
+ { token: 'number', pattern: /\b(0x[\da-fA-F]+|0b[01]+|\d+(\.\d+)?)\b/, light: '#005cc5', dark: '#79c0ff' },
730
+ { token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, light: '#6f42c1', dark: '#d2a8ff' },
731
+ { token: 'op', pattern: /[+\-*/%=<>!&|^~]+/, light: '#069598', dark: '#56b6c2' }
732
+ ];
733
+
734
+ const markdown = [
735
+ // 注释: 这是 Markdown 中的行内注释
736
+ { token: 'comment', pattern: /<!--[\s\S]*?-->/, light: '#6a737d', dark: '#8b949e' },
737
+ // 标题: 通过 `#` 来定义标题
738
+ { token: 'heading', pattern: /(^|\n)(#{1,6})\s*(.+)/, light: '#e36209', dark: '#ffa657' },
739
+ // 粗体: **text** 或 __text__
740
+ { token: 'bold', pattern: /\*\*([^*]+)\*\*|__([^_]+)__/g, light: '#d73a49', dark: '#ff7b72' },
741
+ // 斜体: *text* 或 _text_
742
+ { token: 'italic', pattern: /\*([^*]+)\*|_([^_]+)_/g, light: '#032f62', dark: '#a5d6ff' },
743
+ // 链接: [text](url)
744
+ { token: 'link', pattern: /\[([^\]]+)\]\(([^)]+)\)/g, light: '#0288d1', dark: '#80c0ff' },
745
+ // 行内代码: `code`
746
+ { token: 'inline-code', pattern: /`([^`]+)`/g, light: '#032f62', dark: '#98c379' },
747
+ // 代码块: ```code```
748
+ { token: 'code-block', pattern: /```([^\n]+)\n([\s\S]*?)```/g, light: '#24292e', dark: '#c9d1d9' },
749
+ // 列表项: - item 或 * item
750
+ { token: 'list-item', pattern: /(^|\n)([-*])\s+(.+)/g, light: '#5c6e7c', dark: '#8b949e' },
751
+ // 引用: > text
752
+ { token: 'quote', pattern: /(^|\n)>[ \t]*(.+)/g, light: '#6f42c1', dark: '#d2a8ff' },
753
+ // 图片: ![alt](url)
754
+ { token: 'image', pattern: /!\[([^\]]+)\]\(([^)]+)\)/g, light: '#d73a49', dark: '#ff7b72' },
755
+ // 分割线: ---
756
+ { token: 'hr', pattern: /^(---|___|\*\*\*)\s*$/gm, light: '#24292e', dark: '#c9d1d9' },
757
+ // 强调和删除: ~~text~~
758
+ { token: 'strikethrough', pattern: /~~([^~]+)~~/g, light: '#e36209', dark: '#ffa657' },
759
+ // 表格: | header1 | header2 |
760
+ { token: 'table', pattern: /\|([^\|]+)\|([^\|]+)\|/g, light: '#5c6e7c', dark: '#8b949e' }
761
+ ];
762
+
763
+ const typescript = [
764
+ { token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
765
+ { token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, light: '#032f62', dark: '#98c379' },
766
+ { token: 'decorator', pattern: /@[a-zA-Z_]\w*/, light: '#953800', dark: '#ffa657' },
767
+ { token: 'keyword', pattern: /\b(abstract|as|async|await|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|is|keyof|let|module|namespace|new|package|private|protected|public|readonly|return|set|static|super|switch|this|throw|try|type|typeof|var|while|with|yield)\b/, light: '#d73a49', dark: '#ff7b72' },
768
+ { token: 'builtin', pattern: /\b(any|boolean|never|number|string|symbol|unknown|void|undefined|null|true|false|console|window|document)\b/, light: '#e36209', dark: '#ffa657' },
769
+ { token: 'type', pattern: /\b[A-Z]\w*\b/, light: '#005cc5', dark: '#79c0ff' },
770
+ { token: 'number', pattern: /\b(0x[\da-fA-F]+|0b[01]+|\d+(\.\d+)?)\b/, light: '#005cc5', dark: '#79c0ff' },
771
+ { token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, light: '#6f42c1', dark: '#d2a8ff' },
772
+ { token: 'op', pattern: /(\?\.|![:\.]|[+\-*/%=<>!&|^~]+)/, light: '#089ba6', dark: '#79c0ff' }
773
+ ];
774
+
775
+ const COMMA = ',';
776
+
777
+ const SPACE = ' ';
778
+
779
+ const trim = (str, placement = '') => {
780
+ if (typeof str !== 'string') {
781
+ console.warn('Expected a string input');
782
+ return '';
783
+ }
784
+ switch (placement) {
785
+ case 'start':
786
+ return str.trimStart();
787
+ case 'end':
788
+ return str.trimEnd();
789
+ case 'both':
790
+ return str.trim();
791
+ case 'global':
792
+ return str.replace(/[\s\r\n]+/g, '');
793
+ default:
794
+ return str.trim().replace(/[\s\r\n]+/g, ' '); // Default behavior, trims both ends and replaces inner spaces
795
+ }
796
+ };
797
+
798
+ const parseClasses = (data) => {
799
+ let separator, result = [];
800
+ if (Array.isArray(data)) {
801
+ // If data is already an array, filter out invalid values
802
+ result = data.filter((k) => k && typeof k === 'string');
803
+ }
804
+ else {
805
+ // Trim the input string and handle multiple spaces
806
+ data = trim(data);
807
+ // Use comma as the separator if present, otherwise use space
808
+ separator = data.includes(COMMA) ? COMMA : SPACE;
809
+ result = data.split(separator);
810
+ }
811
+ // Trim each item globally and filter out any empty strings
812
+ return result.map((k) => trim(k, 'global')).filter(Boolean);
813
+ };
814
+
815
+ const rtlStyle = (name = '') => `
816
+ <style>
817
+ :where([dir="rtl"]) .icax-${name},
818
+ :where(:dir(rtl)) .icax-${name} {
819
+ transform: scaleX(-1);
820
+ transform-origin: center;
821
+ }
822
+ </style>
823
+ `;
824
+
825
+ const wrap = (content, fun, isRtl = false, options) => {
826
+ const size = options?.size || '1em', color = options?.color || 'currentColor', thickness = options?.thickness || 2, classes = options?.classes ? parseClasses(options.classes).join(' ') : '',
827
+ // 得到 "icax-left"
828
+ origName = fun.name.replace(/([A-Z])/g, "-$1").toLowerCase();
829
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}"
830
+ stroke-width="${thickness}" stroke-linecap="round" stroke-linejoin="round" class="${origName} ${classes}">
831
+ ${isRtl ? rtlStyle(origName.split('-')[1]) : ''}
832
+ ${content}
833
+ </svg>`;
834
+ };
835
+
836
+ const icaxCheck = (options) => wrap(`<polyline points="20 6 9 17 4 12"></polyline>`, icaxCheck, false, options);
837
+
838
+ const icaxCopy = (options) => wrap(`<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>`, icaxCopy, false, options);
839
+
840
+ const copy = {
841
+ name: 'copy',
842
+ icon: icaxCopy(),
843
+ action: function (arg) {
844
+ arg.wrapEl.onclick = () => {
845
+ //this只是组件实例
846
+ navigator.clipboard.writeText(this.source)
847
+ .then(() => {
848
+ console.log('Text successfully copied to clipboard');
849
+ arg.iconEl.innerHTML = icaxCheck();
850
+ arg.iconEl.toggleAttribute('disabled', true);
851
+ setTimeout(() => {
852
+ //恢复
853
+ arg.iconEl.removeAttribute('disabled');
854
+ arg.iconEl.innerHTML = icaxCopy();
855
+ }, 2000);
856
+ })
857
+ .catch(err => {
858
+ console.error('Error copying text to clipboard: ', err);
859
+ });
860
+ };
861
+ }
862
+ };
863
+
864
+ exports.CoaxElem = CoaxElem;
865
+ exports.copy = copy;
866
+ exports.css = css;
867
+ exports.html = html;
868
+ exports.javascript = javascript;
869
+ exports.markdown = markdown;
870
+ exports.typescript = typescript;