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