@codady/coax 0.0.3 → 0.0.5

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/dist/coax.umd.js CHANGED
@@ -1,10 +1,10 @@
1
1
 
2
2
  /*!
3
- * @since Last modified: 2026-1-12 9:47:5
3
+ * @since Last modified: 2026-1-12 15:14:48
4
4
  * @name Coax event management system.
5
- * @version 0.0.3
5
+ * @version 0.0.5
6
6
  * @author AXUI development team <3217728223@qq.com>
7
- * @description Coax is a custom web component that enables syntax highlighting for various programming languages inside your HTML documents.
7
+ * @description Coax is a lightweight web component for elegant code display with syntax highlighting, typewriter effects, and theme switching. Supports JavaScript, HTML, CSS, TypeScript, and Markdown with copy tools and customization.
8
8
  * @see {@link https://coax.axui.cn|Official website}
9
9
  * @see {@link https://github.com/codady/coax/issues|github issues}
10
10
  * @see {@link https://gitee.com/codady/coax/issues|Gitee issues}
@@ -22,6 +22,170 @@
22
22
 
23
23
  const NAMESPACE = 'ax';
24
24
 
25
+ const html = [
26
+ { token: 'comment', pattern: /&lt;!--[\s\S]*?--&gt;/, light: '#999999', dark: '#6e7681' },
27
+ { token: 'doctype', pattern: /&lt;!DOCTYPE[\s\S]*?&gt;/i, light: '#6a737d', dark: '#8b949e' },
28
+ // 匹配标签名: <div, </div
29
+ { token: 'tag', pattern: /&lt;\/?[a-zA-Z0-9]+/, light: '#22863a', dark: '#7ee787' },
30
+ // 匹配属性名: class=
31
+ { token: 'attr', pattern: /[a-zA-Z-]+(?=\s*=\s*)/, light: '#6f42c1', dark: '#d2a8ff' },
32
+ // 匹配属性值: "value"
33
+ { token: 'string', pattern: /(['"])(?:\\.|[^\\])*?\1/, light: '#032f62', dark: '#a5d6ff' },
34
+ // 匹配标签闭合: >, />
35
+ { token: 'bracket', pattern: /\/?&gt;/, light: '#24292e', dark: '#c9d1d9' }
36
+ ];
37
+
38
+ const javascript = [
39
+ { token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
40
+ { token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, light: '#032f62', dark: '#98c379' },
41
+ { 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' },
42
+ { token: 'builtin', pattern: /\b(console|window|document|Math|JSON|true|false|null|undefined|Object|Array|Promise|Number|String|Boolean)\b/, light: '#e36209', dark: '#ffa657' },
43
+ { token: 'number', pattern: /\b(0x[\da-fA-F]+|0b[01]+|\d+(\.\d+)?)\b/, light: '#005cc5', dark: '#79c0ff' },
44
+ { token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, light: '#6f42c1', dark: '#d2a8ff' },
45
+ { token: 'op', pattern: /[+\-*/%=<>!&|^~]+/, light: '#069598', dark: '#56b6c2' }
46
+ ];
47
+
48
+ const markdown = [
49
+ // 注释: 这是 Markdown 中的行内注释
50
+ { token: 'comment', pattern: /<!--[\s\S]*?-->/, light: '#6a737d', dark: '#8b949e' },
51
+ // 标题: 通过 `#` 来定义标题
52
+ { token: 'heading', pattern: /(^|\n)(#{1,6})\s*(.+)/, light: '#e36209', dark: '#ffa657' },
53
+ // 粗体: **text** 或 __text__
54
+ { token: 'bold', pattern: /\*\*([^*]+)\*\*|__([^_]+)__/g, light: '#d73a49', dark: '#ff7b72' },
55
+ // 斜体: *text* 或 _text_
56
+ { token: 'italic', pattern: /\*([^*]+)\*|_([^_]+)_/g, light: '#032f62', dark: '#a5d6ff' },
57
+ // 链接: [text](url)
58
+ { token: 'link', pattern: /\[([^\]]+)\]\(([^)]+)\)/g, light: '#0288d1', dark: '#80c0ff' },
59
+ // 行内代码: `code`
60
+ { token: 'inline-code', pattern: /`([^`]+)`/g, light: '#032f62', dark: '#98c379' },
61
+ // 代码块: ```code```
62
+ { token: 'code-block', pattern: /```([^\n]+)\n([\s\S]*?)```/g, light: '#24292e', dark: '#c9d1d9' },
63
+ // 列表项: - item 或 * item
64
+ { token: 'list-item', pattern: /(^|\n)([-*])\s+(.+)/g, light: '#5c6e7c', dark: '#8b949e' },
65
+ // 引用: > text
66
+ { token: 'quote', pattern: /(^|\n)>[ \t]*(.+)/g, light: '#6f42c1', dark: '#d2a8ff' },
67
+ // 图片: ![alt](url)
68
+ { token: 'image', pattern: /!\[([^\]]+)\]\(([^)]+)\)/g, light: '#d73a49', dark: '#ff7b72' },
69
+ // 分割线: ---
70
+ { token: 'hr', pattern: /^(---|___|\*\*\*)\s*$/gm, light: '#24292e', dark: '#c9d1d9' },
71
+ // 强调和删除: ~~text~~
72
+ { token: 'strikethrough', pattern: /~~([^~]+)~~/g, light: '#e36209', dark: '#ffa657' },
73
+ // 表格: | header1 | header2 |
74
+ { token: 'table', pattern: /\|([^\|]+)\|([^\|]+)\|/g, light: '#5c6e7c', dark: '#8b949e' }
75
+ ];
76
+
77
+ const typescript = [
78
+ { token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
79
+ { token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, light: '#032f62', dark: '#98c379' },
80
+ { token: 'decorator', pattern: /@[a-zA-Z_]\w*/, light: '#953800', dark: '#ffa657' },
81
+ { 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' },
82
+ { token: 'builtin', pattern: /\b(any|boolean|never|number|string|symbol|unknown|void|undefined|null|true|false|console|window|document)\b/, light: '#e36209', dark: '#ffa657' },
83
+ { token: 'type', pattern: /\b[A-Z]\w*\b/, light: '#005cc5', dark: '#79c0ff' },
84
+ { token: 'number', pattern: /\b(0x[\da-fA-F]+|0b[01]+|\d+(\.\d+)?)\b/, light: '#005cc5', dark: '#79c0ff' },
85
+ { token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, light: '#6f42c1', dark: '#d2a8ff' },
86
+ { token: 'op', pattern: /(\?\.|![:\.]|[+\-*/%=<>!&|^~]+)/, light: '#089ba6', dark: '#79c0ff' }
87
+ ];
88
+
89
+ const COMMA$1 = ',';
90
+
91
+ const SPACE$1 = ' ';
92
+
93
+ const trim$1 = (str, placement = '') => {
94
+ if (typeof str !== 'string') {
95
+ console.warn('Expected a string input');
96
+ return '';
97
+ }
98
+ switch (placement) {
99
+ case 'start':
100
+ return str.trimStart();
101
+ case 'end':
102
+ return str.trimEnd();
103
+ case 'both':
104
+ return str.trim();
105
+ case 'global':
106
+ return str.replace(/[\s\r\n]+/g, '');
107
+ default:
108
+ return str.trim().replace(/[\s\r\n]+/g, ' '); // Default behavior, trims both ends and replaces inner spaces
109
+ }
110
+ };
111
+
112
+ const parseClasses$1 = (data) => {
113
+ let separator, result = [];
114
+ if (Array.isArray(data)) {
115
+ // If data is already an array, filter out invalid values
116
+ result = data.filter((k) => k && typeof k === 'string');
117
+ }
118
+ else {
119
+ // Trim the input string and handle multiple spaces
120
+ data = trim$1(data);
121
+ // Use comma as the separator if present, otherwise use space
122
+ separator = data.includes(COMMA$1) ? COMMA$1 : SPACE$1;
123
+ result = data.split(separator);
124
+ }
125
+ // Trim each item globally and filter out any empty strings
126
+ return result.map((k) => trim$1(k, 'global')).filter(Boolean);
127
+ };
128
+
129
+ const rtlStyle = (name = '') => `
130
+ <style>
131
+ :where([dir="rtl"]) .icax-${name},
132
+ :where(:dir(rtl)) .icax-${name} {
133
+ transform: scaleX(-1);
134
+ transform-origin: center;
135
+ }
136
+ </style>
137
+ `;
138
+
139
+ const wrap = (content, fun, isRtl = false, options) => {
140
+ const size = options?.size || '1em', color = options?.color || 'currentColor', thickness = options?.thickness || 2, classes = options?.classes ? parseClasses$1(options.classes).join(' ') : '',
141
+ // 得到 "icax-left"
142
+ origName = fun.name.replace(/([A-Z])/g, "-$1").toLowerCase();
143
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}"
144
+ stroke-width="${thickness}" stroke-linecap="round" stroke-linejoin="round" class="${origName} ${classes}">
145
+ ${isRtl ? rtlStyle(origName.split('-')[1]) : ''}
146
+ ${content}
147
+ </svg>`;
148
+ };
149
+
150
+ const icaxCheck = (options) => wrap(`<polyline points="20 6 9 17 4 12"></polyline>`, icaxCheck, false, options);
151
+
152
+ 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);
153
+
154
+ const copy = {
155
+ name: 'copy',
156
+ icon: icaxCopy(),
157
+ action: function (arg) {
158
+ arg.wrapEl.onclick = () => {
159
+ //this只是组件实例
160
+ navigator.clipboard.writeText(this.source)
161
+ .then(() => {
162
+ console.log('Text successfully copied to clipboard');
163
+ arg.iconEl.innerHTML = icaxCheck();
164
+ arg.iconEl.toggleAttribute('disabled', true);
165
+ setTimeout(() => {
166
+ //恢复
167
+ arg.iconEl.removeAttribute('disabled');
168
+ arg.iconEl.innerHTML = icaxCopy();
169
+ }, 2000);
170
+ })
171
+ .catch(err => {
172
+ console.error('Error copying text to clipboard: ', err);
173
+ });
174
+ };
175
+ }
176
+ };
177
+
178
+ const css = [
179
+ { token: 'comment', pattern: /\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
180
+ { token: 'value', pattern: /(?:'|")(?:\\.|[^\\'"\b])*?(?:'|")/, light: '#032f62', dark: '#a5d6ff' },
181
+ { token: 'func', pattern: /[a-z-]+\(?=/, light: '#e36209', dark: '#ffa657' },
182
+ { token: 'property', pattern: /[a-z-]+(?=\s*:)/, light: '#005cc5', dark: '#79c0ff' },
183
+ { token: 'selector', pattern: /[.#a-z0-9, \n\t>:+()_-]+(?=\s*\{)/i, light: '#22863a', dark: '#7ee787' },
184
+ { token: 'unit', pattern: /(?<=\d)(px|em|rem|%|vh|vw|ms|s|deg)/, light: '#d73a49', dark: '#ff7b72' },
185
+ { token: 'number', pattern: /\b\d+(\.\d+)?\b/, light: '#005cc5', dark: '#79c0ff' },
186
+ { token: 'punct', pattern: /[{}();:]/, light: '#24292e', dark: '#c9d1d9' }
187
+ ];
188
+
25
189
  const typeWriter = (text, options) => {
26
190
  const speed = options.speed || 100; // Set typing speed (default to 100ms per character)
27
191
  return new Promise((resolve) => {
@@ -49,11 +213,11 @@
49
213
  });
50
214
  };
51
215
 
52
- const COMMA$1 = ',';
216
+ const COMMA = ',';
53
217
 
54
- const SPACE$1 = ' ';
218
+ const SPACE = ' ';
55
219
 
56
- const trim$1 = (str, placement = 'compress') => {
220
+ const trim = (str, placement = 'compress') => {
57
221
  if (typeof str !== 'string') {
58
222
  console.warn('Expected a string input');
59
223
  return '';
@@ -72,7 +236,7 @@
72
236
  }
73
237
  };
74
238
 
75
- const parseClasses$1 = (data) => {
239
+ const parseClasses = (data) => {
76
240
  let separator, result = [];
77
241
  if (Array.isArray(data)) {
78
242
  // If data is already an array, filter out invalid values
@@ -80,13 +244,13 @@
80
244
  }
81
245
  else {
82
246
  // Trim the input string and handle multiple spaces
83
- data = trim$1(data);
247
+ data = trim(data);
84
248
  // Use comma as the separator if present, otherwise use space
85
- separator = data.includes(COMMA$1) ? COMMA$1 : SPACE$1;
249
+ separator = data.includes(COMMA) ? COMMA : SPACE;
86
250
  result = data.split(separator);
87
251
  }
88
252
  // Trim each item globally and filter out any empty strings
89
- return result.map((k) => trim$1(k, 'global')).filter(Boolean);
253
+ return result.map((k) => trim(k, 'global')).filter(Boolean);
90
254
  };
91
255
 
92
256
  const getDataType = (obj) => {
@@ -232,7 +396,7 @@
232
396
  const ALIAS = 'rep';
233
397
 
234
398
  const addClasses = (target, classes, intercept) => {
235
- const el = getEl(target), arr = parseClasses$1(classes);
399
+ const el = getEl(target), arr = parseClasses(classes);
236
400
  if (!el || arr.length === 0) {
237
401
  return;
238
402
  }
@@ -272,36 +436,35 @@
272
436
  return toolsEl;
273
437
  };
274
438
 
275
- class CoaxElem extends HTMLElement {
439
+ class Coax extends HTMLElement {
276
440
  // A static Map to hold the configuration for different languages
277
441
  static languages = new Map();
442
+ // A static array to hold the tools registered with the component
278
443
  static tools = [];
279
- source;
280
- _renderQueued = false;
281
- baseStylesEl;
282
- themeStylesEl;
283
- dynamicStylesEl;
284
- highlightedCodeEl;
285
- headerEl;
286
- codeNameEl;
287
- codeToolsEl;
288
- codeBodyEl;
289
- alias;
290
- lineString;
291
- speed;
292
- autoScroll;
444
+ source; // Source code content to be highlighted
445
+ _renderQueued = false; // Flag to prevent multiple render requests
446
+ baseStylesEl; // Element for base styles
447
+ themeStylesEl; // Element for theme styles
448
+ dynamicStylesEl; // Element for dynamic styles
449
+ highlightEl; // Element that holds the highlighted code
450
+ headerEl; // Header element (for code name, tools, etc.)
451
+ codeNameEl; // Code name element (shows language or alias)
452
+ codeToolsEl; // Code tools element (for interactive tools like copy)
453
+ codeBodyEl; // Code body element (the container for the code)
454
+ lang = 'plain'; // Language of the code (default is plain text)
455
+ alias = 'Plain Text'; // Alias name for the language (default is 'Plain Text')
456
+ lineString = ''; // The current line's string being typed
457
+ lastLineString = ''; // The last line's string
458
+ speed = 5; // Speed of the typing effect (higher is slower)
459
+ autoScroll = true; // Flag to enable/disable auto-scrolling
460
+ canListen = true; // Flag to enable/disable auto-scrolling
293
461
  constructor() {
294
462
  super();
295
463
  // Attach a Shadow DOM to the custom element
296
464
  this.attachShadow({ mode: 'open' });
297
465
  // Remove leading/trailing whitespace from the raw code content
298
466
  this.source = this.textContent?.replace(/^\s*\n|\n\s*$/g, '') || '';
299
- this.lang = 'plain';
300
- this.alias = 'Plain Text';
301
- this.lineString = '';
302
- this.speed = 5;
303
- this.autoScroll = true;
304
- // 1. 初始化基础骨架
467
+ // Initialize the basic structure of the component
305
468
  this.shadowRoot.innerHTML = `
306
469
  <style id="base-styles">
307
470
  :host {
@@ -431,10 +594,10 @@
431
594
  <style id="theme-styles"></style>
432
595
  <div id="code-header"><span id="code-name">${this.alias}</span><div id="code-tools"></div></div>
433
596
  <div id="code-body">
434
- <pre><code id="highlight-code"></code></pre>
597
+ <pre><code id="highlight"></code></pre>
435
598
  </div>
436
599
  `;
437
- // 缓存引用
600
+ // Cache references to various elements
438
601
  this.baseStylesEl = getEl('#base-styles', this.shadowRoot);
439
602
  this.themeStylesEl = getEl('#theme-styles', this.shadowRoot);
440
603
  this.dynamicStylesEl = getEl('#dynamic-styles', this.shadowRoot);
@@ -442,10 +605,10 @@
442
605
  this.codeNameEl = getEl('#code-name', this.shadowRoot);
443
606
  this.codeToolsEl = getEl('#code-tools', this.shadowRoot);
444
607
  this.codeBodyEl = getEl('#code-body', this.shadowRoot);
445
- this.highlightedCodeEl = getEl('#highlight-code', this.shadowRoot);
608
+ this.highlightEl = getEl('#highlight', this.shadowRoot);
446
609
  this.codeBodyEl.addEventListener('scroll', () => {
447
610
  let flag = this.codeBodyEl.scrollTop + this.codeBodyEl.clientHeight < this.codeBodyEl.scrollHeight;
448
- // 检测是否是用户手动滚动
611
+ // Check if the user manually scrolled
449
612
  this.autoScroll = !flag;
450
613
  });
451
614
  }
@@ -454,11 +617,11 @@
454
617
  // Store the language configuration in the static map
455
618
  this.languages.set(name, { ...config });
456
619
  }
457
- //注册工具箱
620
+
458
621
  static addTools(items) {
459
- CoaxElem.tools = items;
622
+ Coax.tools = items;
460
623
  }
461
- //加入工具箱
624
+
462
625
  mountTools(toolItems) {
463
626
  requestAnimationFrame(() => {
464
627
  this.codeToolsEl.innerHTML = '';
@@ -469,31 +632,32 @@
469
632
  this.codeToolsEl.appendChild(createTools(items));
470
633
  });
471
634
  }
472
- // Called when the element is connected to the DOM
635
+
473
636
  connectedCallback() {
474
637
  this.render();
475
638
  }
476
- // Observed attributes for changes
639
+
477
640
  static get observedAttributes() { return ['lang', 'height', 'max-height', 'tools', 'speed']; }
478
641
 
479
642
  attributeChangedCallback(name, oldVal, newVal) {
480
- if (oldVal === newVal)
643
+ if (oldVal === newVal || !this.canListen)
481
644
  return;
482
645
  if (name === 'height' || name === 'max-height') {
483
646
  this.updateStyleByRegExp(name, newVal);
484
647
  }
485
648
  if (name === 'speed') {
649
+ // Convert to integer (0 or 1)
486
650
  this.speed = ~~(!!newVal);
487
651
  }
488
652
  if (name === 'lang') {
489
- //更新当前语言
653
+ // Update the language and re-render
490
654
  this.lang = newVal;
491
655
  this.render();
492
656
  }
493
657
  if (name === 'tools') {
494
658
  if (!newVal)
495
659
  this.codeToolsEl.innerHTML = '';
496
- const tools = parseClasses$1(newVal), toolItems = CoaxElem.tools.filter(k => tools.includes(k.name));
660
+ const tools = parseClasses(newVal), toolItems = Coax.tools.filter(k => tools.includes(k.name));
497
661
  if (!toolItems.length)
498
662
  return;
499
663
  this.mountTools(toolItems);
@@ -507,11 +671,13 @@
507
671
  // 替换为新的属性值
508
672
  this.baseStylesEl.textContent = this.baseStylesEl.textContent.replace(regex, `;\n${prop}: ${value};`);
509
673
  }
674
+
510
675
  getCssPrefix(config) {
511
676
  return (config?.cssPrefix || this.lang).replace(/[^a-zA-Z0-9_-]/g, '\\$&');
512
677
  }
678
+
513
679
  getHighLightString(string, config) {
514
- config = config || CoaxElem.languages.get(this.lang);
680
+ config = config || Coax.languages.get(this.lang);
515
681
  if (!config)
516
682
  return string;
517
683
  // 如果找到了配置,则进行高亮处理
@@ -523,19 +689,21 @@
523
689
  return i !== -1 && config.rules[i] ? `<span class="${NAMESPACE}-${cssPrefix}-${config.rules[i].token}">${match}</span>` : match;
524
690
  });
525
691
  }
692
+
526
693
  createLineWrap(index, startIndex) {
527
694
  let dataIndex = 0;
528
695
  if (index == null && startIndex == null) {
529
- dataIndex = this.highlightedCodeEl.children.length;
696
+ dataIndex = this.highlightEl.children.length;
530
697
  }
531
698
  else {
532
- const start = startIndex || this.highlightedCodeEl.children.length;
699
+ const start = startIndex || this.highlightEl.children.length;
533
700
  dataIndex = start + index;
534
701
  }
535
702
  return createEl('div', { 'data-index': dataIndex }, '<div></div>');
536
703
  }
704
+
537
705
  getLineToFill(codeWrap, line, config) {
538
- config = config || CoaxElem.languages.get(this.lang);
706
+ config = config || Coax.languages.get(this.lang);
539
707
  let highlightedLine = this.getHighLightString(line, config);
540
708
  // 将高亮后的内容填充到 div 中
541
709
  codeWrap.innerHTML = highlightedLine;
@@ -543,7 +711,7 @@
543
711
  ;
544
712
 
545
713
  async highlight(newCode) {
546
- const config = CoaxElem.languages.get(this.lang), startIndex = this.highlightedCodeEl.children.length,
714
+ const config = Coax.languages.get(this.lang), startIndex = this.highlightEl.children.length,
547
715
  // 将新源码按行分割
548
716
  newCodeLines = newCode ? newCode.split('\n') : [];
549
717
  //更新别名
@@ -558,9 +726,9 @@
558
726
  // 创建一个 div 包裹每一行
559
727
  const lineWrap = this.createLineWrap(index, startIndex), codeWrap = lineWrap.lastElementChild;
560
728
  //标记完成
561
- this.closeLine(lineWrap);
729
+ lineWrap.completed = true;
562
730
  if (this.hasAttribute('speed')) {
563
- this.highlightedCodeEl.appendChild(lineWrap);
731
+ this.highlightEl.appendChild(lineWrap);
564
732
  //流式打字
565
733
  await typeWriter(lineString, {
566
734
  speed: this.speed,
@@ -574,20 +742,22 @@
574
742
  //直接打出
575
743
  this.getLineToFill(codeWrap, lineString, config);
576
744
  //
577
- this.highlightedCodeEl.appendChild(lineWrap);
745
+ this.highlightEl.appendChild(lineWrap);
578
746
  }
579
747
  }
580
748
  //滚动到最底部
581
749
  this.autoScrollCode();
582
750
  return true;
583
751
  }
752
+
584
753
  autoScrollCode() {
585
754
  if (this.autoScroll) {
586
755
  this.codeBodyEl.scrollTop = this.codeBodyEl.scrollHeight;
587
756
  }
588
757
  }
758
+
589
759
  injectThemeStyles() {
590
- const config = CoaxElem.languages.get(this.lang);
760
+ const config = Coax.languages.get(this.lang);
591
761
  if (!config)
592
762
  return;
593
763
  // Get language name, fallback to 'js' if not provided
@@ -609,13 +779,13 @@
609
779
  .${NAMESPACE}-${cssPrefix}-${rule.token} { color: var(--${NAMESPACE}-${cssPrefix}-${rule.token},${rule.dark}); }` : ``} `).join('\n');
610
780
  schemeStyles += `}`;
611
781
  // Set the inner HTML of the shadow root, including styles and highlighted code
612
- // 2. 精确更新 DOM 节点而非重写 ShadowRoot
613
782
  this.dynamicStylesEl.textContent = lightStyles + darkStyles + schemeStyles;
614
783
  //附加主题样式
615
784
  if (config?.themeStyles) {
616
785
  this.themeStylesEl.textContent = config.themeStyles;
617
786
  }
618
787
  }
788
+
619
789
  updateName(config) {
620
790
  if (this.hasAttribute('unnamed'))
621
791
  return;
@@ -631,21 +801,18 @@
631
801
 
632
802
  render(code = this.source) {
633
803
  //同时多次改变属性,只执行一次
634
- // 如果已经在队列中,则直接返回
635
804
  if (this._renderQueued)
636
805
  return;
637
806
  this._renderQueued = true;
638
- // 使用 requestAnimationFrame 将渲染推迟到下一帧
639
- requestAnimationFrame(async () => {
640
- this.clear();
641
- this.injectThemeStyles();
642
- //一次性渲染
643
- await this.highlight(code);
644
- this._renderQueued = false;
645
- });
807
+ this.clear();
808
+ this.injectThemeStyles();
809
+ //一次性渲染
810
+ this.highlight(code);
811
+ this._renderQueued = false;
646
812
  }
813
+
647
814
  clear() {
648
- this.highlightedCodeEl.innerHTML = this.source = this.lineString = '';
815
+ this.highlightEl.innerHTML = this.source = this.lineString = '';
649
816
  }
650
817
 
651
818
  async replace(newCode) {
@@ -659,36 +826,46 @@
659
826
  // 高亮新的部分
660
827
  await this.highlight(newCode);
661
828
  }
662
- getActiveCodeWrap() {
663
- const lastChild = this.highlightedCodeEl.lastElementChild, lastLine = !lastChild || this.highlightedCodeEl.lastElementChild?.completed ?
829
+
830
+ getLastLine() {
831
+ const lastChild = this.highlightEl.lastElementChild, lastLine = !lastChild || this.highlightEl.lastElementChild?.completed ?
664
832
  this.createLineWrap() : lastChild;
665
833
  return {
666
834
  lineWrap: lastLine,
667
835
  codeWrap: lastLine.lastElementChild
668
836
  };
669
837
  }
670
- closeLine(lineWrap) {
671
- lineWrap = lineWrap || this.highlightedCodeEl.lastElementChild;
672
- lineWrap && (lineWrap.completed = true);
838
+
839
+ close() {
840
+ const lineWrap = this.highlightEl.lastElementChild;
841
+ if (!lineWrap)
842
+ return;
843
+ lineWrap.completed = true;
844
+ //行结束前保存
845
+ this.lastLineString = this.lineString;
846
+ this.getLineToFill(lineWrap?.lastElementChild, this.lineString);
847
+ //一行结束,清空临时行文本
848
+ this.lineString = '';
673
849
  }
674
- openLine(lineWrap) {
675
- lineWrap = lineWrap || this.highlightedCodeEl.lastElementChild;
676
- lineWrap && (lineWrap.completed = false);
850
+
851
+ open() {
852
+ const lineWrap = this.highlightEl.lastElementChild;
853
+ if (!lineWrap)
854
+ return;
855
+ lineWrap.completed = false;
856
+ //恢复最后一行字符串
857
+ (lineWrap?.lastElementChild).textContent = this.lineString = this.lastLineString;
677
858
  }
678
- stream(str, forceEnd = false) {
679
- const { lineWrap, codeWrap } = this.getActiveCodeWrap();
680
- this.highlightedCodeEl.appendChild(lineWrap);
859
+
860
+ stream(str, forceClose = false) {
861
+ const { lineWrap, codeWrap } = this.getLastLine();
862
+ this.highlightEl.appendChild(lineWrap);
681
863
  //汇集
682
864
  this.source += str;
683
865
  //如果没有遇到换行符号,也可以强制结束
684
- forceEnd && this.closeLine(lineWrap);
685
- if (str.startsWith('\n') || str.startsWith('\r')) {
866
+ if (forceClose || (str.startsWith('\n') || str.endsWith('\n'))) {
686
867
  //标记完成
687
- this.closeLine(lineWrap);
688
- //渲染
689
- this.getLineToFill(codeWrap, this.lineString);
690
- //一行结束,清空临时行文本
691
- this.lineString = '';
868
+ this.close();
692
869
  }
693
870
  else {
694
871
  //插入文本
@@ -701,171 +878,6 @@
701
878
  }
702
879
  }
703
880
 
704
- const html = [
705
- { token: 'comment', pattern: /&lt;!--[\s\S]*?--&gt;/, light: '#999999', dark: '#6e7681' },
706
- { token: 'doctype', pattern: /&lt;!DOCTYPE[\s\S]*?&gt;/i, light: '#6a737d', dark: '#8b949e' },
707
- // 匹配标签名: <div, </div
708
- { token: 'tag', pattern: /&lt;\/?[a-zA-Z0-9]+/, light: '#22863a', dark: '#7ee787' },
709
- // 匹配属性名: class=
710
- { token: 'attr', pattern: /[a-zA-Z-]+(?=\s*=\s*)/, light: '#6f42c1', dark: '#d2a8ff' },
711
- // 匹配属性值: "value"
712
- { token: 'string', pattern: /(['"])(?:\\.|[^\\])*?\1/, light: '#032f62', dark: '#a5d6ff' },
713
- // 匹配标签闭合: >, />
714
- { token: 'bracket', pattern: /\/?&gt;/, light: '#24292e', dark: '#c9d1d9' }
715
- ];
716
-
717
- const javascript = [
718
- { token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
719
- { token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, light: '#032f62', dark: '#98c379' },
720
- { 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' },
721
- { token: 'builtin', pattern: /\b(console|window|document|Math|JSON|true|false|null|undefined|Object|Array|Promise|Number|String|Boolean)\b/, light: '#e36209', dark: '#ffa657' },
722
- { token: 'number', pattern: /\b(0x[\da-fA-F]+|0b[01]+|\d+(\.\d+)?)\b/, light: '#005cc5', dark: '#79c0ff' },
723
- { token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, light: '#6f42c1', dark: '#d2a8ff' },
724
- { token: 'op', pattern: /[+\-*/%=<>!&|^~]+/, light: '#069598', dark: '#56b6c2' }
725
- ];
726
-
727
- const markdown = [
728
- // 注释: 这是 Markdown 中的行内注释
729
- { token: 'comment', pattern: /<!--[\s\S]*?-->/, light: '#6a737d', dark: '#8b949e' },
730
- // 标题: 通过 `#` 来定义标题
731
- { token: 'heading', pattern: /(^|\n)(#{1,6})\s*(.+)/, light: '#e36209', dark: '#ffa657' },
732
- // 粗体: **text** 或 __text__
733
- { token: 'bold', pattern: /\*\*([^*]+)\*\*|__([^_]+)__/g, light: '#d73a49', dark: '#ff7b72' },
734
- // 斜体: *text* 或 _text_
735
- { token: 'italic', pattern: /\*([^*]+)\*|_([^_]+)_/g, light: '#032f62', dark: '#a5d6ff' },
736
- // 链接: [text](url)
737
- { token: 'link', pattern: /\[([^\]]+)\]\(([^)]+)\)/g, light: '#0288d1', dark: '#80c0ff' },
738
- // 行内代码: `code`
739
- { token: 'inline-code', pattern: /`([^`]+)`/g, light: '#032f62', dark: '#98c379' },
740
- // 代码块: ```code```
741
- { token: 'code-block', pattern: /```([^\n]+)\n([\s\S]*?)```/g, light: '#24292e', dark: '#c9d1d9' },
742
- // 列表项: - item 或 * item
743
- { token: 'list-item', pattern: /(^|\n)([-*])\s+(.+)/g, light: '#5c6e7c', dark: '#8b949e' },
744
- // 引用: > text
745
- { token: 'quote', pattern: /(^|\n)>[ \t]*(.+)/g, light: '#6f42c1', dark: '#d2a8ff' },
746
- // 图片: ![alt](url)
747
- { token: 'image', pattern: /!\[([^\]]+)\]\(([^)]+)\)/g, light: '#d73a49', dark: '#ff7b72' },
748
- // 分割线: ---
749
- { token: 'hr', pattern: /^(---|___|\*\*\*)\s*$/gm, light: '#24292e', dark: '#c9d1d9' },
750
- // 强调和删除: ~~text~~
751
- { token: 'strikethrough', pattern: /~~([^~]+)~~/g, light: '#e36209', dark: '#ffa657' },
752
- // 表格: | header1 | header2 |
753
- { token: 'table', pattern: /\|([^\|]+)\|([^\|]+)\|/g, light: '#5c6e7c', dark: '#8b949e' }
754
- ];
755
-
756
- const typescript = [
757
- { token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
758
- { token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, light: '#032f62', dark: '#98c379' },
759
- { token: 'decorator', pattern: /@[a-zA-Z_]\w*/, light: '#953800', dark: '#ffa657' },
760
- { 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' },
761
- { token: 'builtin', pattern: /\b(any|boolean|never|number|string|symbol|unknown|void|undefined|null|true|false|console|window|document)\b/, light: '#e36209', dark: '#ffa657' },
762
- { token: 'type', pattern: /\b[A-Z]\w*\b/, light: '#005cc5', dark: '#79c0ff' },
763
- { token: 'number', pattern: /\b(0x[\da-fA-F]+|0b[01]+|\d+(\.\d+)?)\b/, light: '#005cc5', dark: '#79c0ff' },
764
- { token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, light: '#6f42c1', dark: '#d2a8ff' },
765
- { token: 'op', pattern: /(\?\.|![:\.]|[+\-*/%=<>!&|^~]+)/, light: '#089ba6', dark: '#79c0ff' }
766
- ];
767
-
768
- const COMMA = ',';
769
-
770
- const SPACE = ' ';
771
-
772
- const trim = (str, placement = '') => {
773
- if (typeof str !== 'string') {
774
- console.warn('Expected a string input');
775
- return '';
776
- }
777
- switch (placement) {
778
- case 'start':
779
- return str.trimStart();
780
- case 'end':
781
- return str.trimEnd();
782
- case 'both':
783
- return str.trim();
784
- case 'global':
785
- return str.replace(/[\s\r\n]+/g, '');
786
- default:
787
- return str.trim().replace(/[\s\r\n]+/g, ' '); // Default behavior, trims both ends and replaces inner spaces
788
- }
789
- };
790
-
791
- const parseClasses = (data) => {
792
- let separator, result = [];
793
- if (Array.isArray(data)) {
794
- // If data is already an array, filter out invalid values
795
- result = data.filter((k) => k && typeof k === 'string');
796
- }
797
- else {
798
- // Trim the input string and handle multiple spaces
799
- data = trim(data);
800
- // Use comma as the separator if present, otherwise use space
801
- separator = data.includes(COMMA) ? COMMA : SPACE;
802
- result = data.split(separator);
803
- }
804
- // Trim each item globally and filter out any empty strings
805
- return result.map((k) => trim(k, 'global')).filter(Boolean);
806
- };
807
-
808
- const rtlStyle = (name = '') => `
809
- <style>
810
- :where([dir="rtl"]) .icax-${name},
811
- :where(:dir(rtl)) .icax-${name} {
812
- transform: scaleX(-1);
813
- transform-origin: center;
814
- }
815
- </style>
816
- `;
817
-
818
- const wrap = (content, fun, isRtl = false, options) => {
819
- const size = options?.size || '1em', color = options?.color || 'currentColor', thickness = options?.thickness || 2, classes = options?.classes ? parseClasses(options.classes).join(' ') : '',
820
- // 得到 "icax-left"
821
- origName = fun.name.replace(/([A-Z])/g, "-$1").toLowerCase();
822
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}"
823
- stroke-width="${thickness}" stroke-linecap="round" stroke-linejoin="round" class="${origName} ${classes}">
824
- ${isRtl ? rtlStyle(origName.split('-')[1]) : ''}
825
- ${content}
826
- </svg>`;
827
- };
828
-
829
- const icaxCheck = (options) => wrap(`<polyline points="20 6 9 17 4 12"></polyline>`, icaxCheck, false, options);
830
-
831
- 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);
832
-
833
- const copy = {
834
- name: 'copy',
835
- icon: icaxCopy(),
836
- action: function (arg) {
837
- arg.wrapEl.onclick = () => {
838
- //this只是组件实例
839
- navigator.clipboard.writeText(this.source)
840
- .then(() => {
841
- console.log('Text successfully copied to clipboard');
842
- arg.iconEl.innerHTML = icaxCheck();
843
- arg.iconEl.toggleAttribute('disabled', true);
844
- setTimeout(() => {
845
- //恢复
846
- arg.iconEl.removeAttribute('disabled');
847
- arg.iconEl.innerHTML = icaxCopy();
848
- }, 2000);
849
- })
850
- .catch(err => {
851
- console.error('Error copying text to clipboard: ', err);
852
- });
853
- };
854
- }
855
- };
856
-
857
- const css = [
858
- { token: 'comment', pattern: /\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
859
- { token: 'value', pattern: /(?:'|")(?:\\.|[^\\'"\b])*?(?:'|")/, light: '#032f62', dark: '#a5d6ff' },
860
- { token: 'func', pattern: /[a-z-]+\(?=/, light: '#e36209', dark: '#ffa657' },
861
- { token: 'property', pattern: /[a-z-]+(?=\s*:)/, light: '#005cc5', dark: '#79c0ff' },
862
- { token: 'selector', pattern: /[.#a-z0-9, \n\t>:+()_-]+(?=\s*\{)/i, light: '#22863a', dark: '#7ee787' },
863
- { token: 'unit', pattern: /(?<=\d)(px|em|rem|%|vh|vw|ms|s|deg)/, light: '#d73a49', dark: '#ff7b72' },
864
- { token: 'number', pattern: /\b\d+(\.\d+)?\b/, light: '#005cc5', dark: '#79c0ff' },
865
- { token: 'punct', pattern: /[{}();:]/, light: '#24292e', dark: '#c9d1d9' }
866
- ];
867
-
868
- const Coax = CoaxElem;
869
881
  //注册语言类型
870
882
  Coax.register('css', {
871
883
  alias: 'CSS',