@codady/coax 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +331 -166
- package/dist/coax.cjs.js +450 -271
- package/dist/coax.cjs.min.js +4 -4
- package/dist/coax.esm.js +444 -271
- package/dist/coax.esm.min.js +4 -4
- package/dist/coax.umd.js +466 -266
- package/dist/coax.umd.min.js +4 -4
- package/examples/.htaccess +0 -0
- package/examples/append-highlight.html +82 -0
- package/examples/color-selector.html +412 -0
- package/examples/deepseek-highlight.html +100 -0
- package/examples/js-highlight.html +1 -1
- package/examples/md-highlight.html +60 -0
- package/examples/nginx.htaccess +0 -0
- package/examples/replace-highlight.html +69 -0
- package/examples/stream-highlight.html +64 -0
- package/examples/theme-highlight.html +69 -0
- package/package.json +4 -4
- package/rollup.config.js +3 -3
- package/script-note.js +2 -2
- package/src/Coax.js +25 -414
- package/src/Coax.ts +28 -443
- package/src/components/Coax.js +528 -0
- package/src/components/Coax.ts +556 -0
- package/src/modules.js +12 -0
- package/src/modules.ts +23 -0
- package/src/rules/css.js +11 -0
- package/src/rules/css.ts +11 -0
- package/src/rules/html.js +13 -0
- package/src/rules/html.ts +13 -0
- package/src/rules/javascript.js +10 -0
- package/src/rules/javascript.ts +10 -0
- package/src/rules/markdown.js +29 -0
- package/src/rules/markdown.ts +41 -0
- package/src/rules/ruleCss - /345/211/257/346/234/254.js" +10 -0
- package/src/rules/ruleHTML - /345/211/257/346/234/254.js" +12 -0
- package/src/rules/ruleJs - /345/211/257/346/234/254.js" +10 -0
- package/src/rules/ruleTs - /345/211/257/346/234/254.js" +12 -0
- package/src/rules/typescript.js +12 -0
- package/src/rules/typescript.ts +12 -0
- package/src/tools/copy.js +26 -0
- package/src/tools/copy.ts +29 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last modified: 2026/01/12 14:09:42
|
|
3
|
+
* Coax - A custom web component for syntax highlighting, code display, and interactive features
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import typeWriter from "@codady/utils/typeWriter";
|
|
8
|
+
import parseClasses from "@codady/utils/parseClasses";
|
|
9
|
+
import NAMESPACE from "@codady/utils/namespace";
|
|
10
|
+
import getEl from "@codady/utils/getEl";
|
|
11
|
+
import createTools, { toolsItem } from "@codady/utils/createTools";
|
|
12
|
+
import createEl from "@codady/utils/createEl";
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// Define the structure for the language configuration
|
|
16
|
+
export interface LanguageRule {
|
|
17
|
+
token: string; // Token representing a specific syntax element (e.g., keyword, string, comment)
|
|
18
|
+
pattern: RegExp; // Regular expression used to match the syntax element
|
|
19
|
+
light?: string; // Optional CSS color for light mode
|
|
20
|
+
dark?: string; // Optional CSS color for dark mode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LanguageConfig {
|
|
24
|
+
rules: LanguageRule[]; // Array of syntax highlighting rules
|
|
25
|
+
alias?: string; // Alias name for the language (e.g., 'JavaScript', 'JS', etc.)
|
|
26
|
+
themeStyles?: string; // Optional internal CSS for language-specific styles
|
|
27
|
+
cssPrefix?: string; // Optional CSS prefix for class names used in syntax highlighting
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class Coax extends HTMLElement {
|
|
31
|
+
// A static Map to hold the configuration for different languages
|
|
32
|
+
static languages = new Map<string, LanguageConfig>();
|
|
33
|
+
|
|
34
|
+
// A static array to hold the tools registered with the component
|
|
35
|
+
static tools: any[] = [];
|
|
36
|
+
|
|
37
|
+
private source: string; // Source code content to be highlighted
|
|
38
|
+
private _renderQueued = false; // Flag to prevent multiple render requests
|
|
39
|
+
private baseStylesEl!: HTMLStyleElement; // Element for base styles
|
|
40
|
+
private themeStylesEl!: HTMLStyleElement; // Element for theme styles
|
|
41
|
+
private dynamicStylesEl!: HTMLStyleElement; // Element for dynamic styles
|
|
42
|
+
private highlightedCodeEl!: HTMLElement; // Element that holds the highlighted code
|
|
43
|
+
private headerEl!: HTMLElement; // Header element (for code name, tools, etc.)
|
|
44
|
+
private codeNameEl!: HTMLElement; // Code name element (shows language or alias)
|
|
45
|
+
private codeToolsEl!: HTMLElement; // Code tools element (for interactive tools like copy)
|
|
46
|
+
private codeBodyEl!: HTMLElement; // Code body element (the container for the code)
|
|
47
|
+
|
|
48
|
+
public lang: string = 'plain'; // Language of the code (default is plain text)
|
|
49
|
+
public alias: string = 'Plain Text'; // Alias name for the language (default is 'Plain Text')
|
|
50
|
+
public lineString: string = ''; // The current line's string being typed
|
|
51
|
+
public lastLineString: string = ''; // The last line's string
|
|
52
|
+
public speed: number = 5; // Speed of the typing effect (higher is slower)
|
|
53
|
+
public autoScroll: boolean = true; // Flag to enable/disable auto-scrolling
|
|
54
|
+
|
|
55
|
+
constructor() {
|
|
56
|
+
super();
|
|
57
|
+
// Attach a Shadow DOM to the custom element
|
|
58
|
+
this.attachShadow({ mode: 'open' });
|
|
59
|
+
// Remove leading/trailing whitespace from the raw code content
|
|
60
|
+
this.source = this.textContent?.replace(/^\s*\n|\n\s*$/g, '') || '';
|
|
61
|
+
// Initialize the basic structure of the component
|
|
62
|
+
(this.shadowRoot as any).innerHTML = `
|
|
63
|
+
<style id="base-styles">
|
|
64
|
+
:host {
|
|
65
|
+
--border-width:1px;
|
|
66
|
+
--border-style:solid;
|
|
67
|
+
--radius:9px;
|
|
68
|
+
--height:auto;
|
|
69
|
+
--max-height:500px;
|
|
70
|
+
--radius:9px;
|
|
71
|
+
--padding:1em;
|
|
72
|
+
--font-size:16px;
|
|
73
|
+
--line-height:1.8;
|
|
74
|
+
--background:rgb(247, 247, 247);
|
|
75
|
+
--border-color:rgb(224, 224, 224);
|
|
76
|
+
--color-code:rgb(51, 51, 51);
|
|
77
|
+
--color-index:rgb(153, 153, 153);
|
|
78
|
+
--color-stripe:rgba(0,0,0,0.04);
|
|
79
|
+
--color-hover:rgba(0,0,0,0.06);
|
|
80
|
+
}
|
|
81
|
+
:host([scheme="dark"]){
|
|
82
|
+
--background: #282c34;
|
|
83
|
+
--border-color: transparent;
|
|
84
|
+
--color-code: #abb2bf;
|
|
85
|
+
--color-index:rgb(153, 153, 153);
|
|
86
|
+
--color-stripe:rgba(255,255,255,0.04);
|
|
87
|
+
--color-hover:rgba(255,255,255,0.06);
|
|
88
|
+
}
|
|
89
|
+
@media (prefers-color-scheme: dark) {
|
|
90
|
+
:host{
|
|
91
|
+
--background: #282c34;
|
|
92
|
+
--border-color: transparent;
|
|
93
|
+
--color-code: #abb2bf;
|
|
94
|
+
--color-index:rgb(153, 153, 153);
|
|
95
|
+
--color-stripe:rgba(255,255,255,0.04);
|
|
96
|
+
--color-hover:rgba(255,255,255,0.06);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
:host {
|
|
100
|
+
font-size: var(--${NAMESPACE}-code-font-size,var(--font-size));
|
|
101
|
+
display: block;
|
|
102
|
+
box-sizing:border-box;
|
|
103
|
+
background:var(--${NAMESPACE}-code-background-color,var(--background));
|
|
104
|
+
color:var(--${NAMESPACE}-code-color,var(--color-code));
|
|
105
|
+
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));
|
|
106
|
+
transition: border-color 0.3s ease,color 0.3s ease;
|
|
107
|
+
border-radius: var(--${NAMESPACE}-code-radius,var(--radius));
|
|
108
|
+
}
|
|
109
|
+
#code-header{
|
|
110
|
+
line-height:calc(var(--${NAMESPACE}-code-line-height,var(--line-height))*1.5);
|
|
111
|
+
padding-inline-start:var(--${NAMESPACE}-code-padding,var(--padding));
|
|
112
|
+
display:flex;
|
|
113
|
+
|
|
114
|
+
>:first-child{
|
|
115
|
+
flex:auto;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
#code-body{
|
|
119
|
+
padding: var(--${NAMESPACE}-code-padding,var(--padding)) 0;
|
|
120
|
+
height:var(--${NAMESPACE}-code-height,var(--height));
|
|
121
|
+
max-height:var(--${NAMESPACE}-code-max-height,var(--max-height));
|
|
122
|
+
overflow:auto;
|
|
123
|
+
}
|
|
124
|
+
pre,code{
|
|
125
|
+
font-family:"Consolas", "Monaco", "Andale Mono", "Ubuntu Mono", "monospace";
|
|
126
|
+
margin:0; padding:0;
|
|
127
|
+
}
|
|
128
|
+
code>div{
|
|
129
|
+
display:flex;
|
|
130
|
+
padding:0 var(--${NAMESPACE}-code-padding,var(--padding));
|
|
131
|
+
line-height: var(--${NAMESPACE}-code-line-height,var(--line-height));
|
|
132
|
+
box-sizing:border-box;
|
|
133
|
+
|
|
134
|
+
>div{
|
|
135
|
+
flex:auto;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
code>div>div:empty:before{
|
|
139
|
+
content:' ';
|
|
140
|
+
}
|
|
141
|
+
:host([indexed]) code>div:before{
|
|
142
|
+
display:inline-flex;
|
|
143
|
+
color:var(--color-index);
|
|
144
|
+
content: attr(data-index);
|
|
145
|
+
min-width:var(--${NAMESPACE}-code-index-width,2em);
|
|
146
|
+
margin-inline-end:var(--${NAMESPACE}-code-padding,8px);
|
|
147
|
+
}
|
|
148
|
+
:host([striped]) code>div:nth-child(odd){
|
|
149
|
+
background-color:var(--color-stripe);
|
|
150
|
+
}
|
|
151
|
+
:host([hoverable]) code>div:hover{
|
|
152
|
+
background-color:var(--color-hover);
|
|
153
|
+
}
|
|
154
|
+
:host([wrapped]) code>div>div{
|
|
155
|
+
white-space: pre-wrap;
|
|
156
|
+
}
|
|
157
|
+
:host([unnamed]) #code-name{
|
|
158
|
+
display:none;
|
|
159
|
+
}
|
|
160
|
+
.${NAMESPACE}-box-tools{
|
|
161
|
+
>*{
|
|
162
|
+
font-size:14px;
|
|
163
|
+
display:inline-flex;
|
|
164
|
+
align-items:center;
|
|
165
|
+
justify-content:center;
|
|
166
|
+
height:2em;
|
|
167
|
+
line-height:2em;
|
|
168
|
+
aspect-ratio:1/1;
|
|
169
|
+
margin-inline-end:8px;
|
|
170
|
+
transition:all 200ms ease;
|
|
171
|
+
border-radius:6px;
|
|
172
|
+
&:hover{
|
|
173
|
+
cursor:pointer;
|
|
174
|
+
background-color:rgba(0,0,0,.04);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
[rep=icon]{
|
|
178
|
+
display:inline-flex;
|
|
179
|
+
align-items:center;
|
|
180
|
+
justify-content:center;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
[disabled]{
|
|
184
|
+
pointer-event:none;
|
|
185
|
+
}
|
|
186
|
+
</style>
|
|
187
|
+
<style id="dynamic-styles"></style>
|
|
188
|
+
<style id="theme-styles"></style>
|
|
189
|
+
<div id="code-header"><span id="code-name">${this.alias}</span><div id="code-tools"></div></div>
|
|
190
|
+
<div id="code-body">
|
|
191
|
+
<pre><code id="highlight-code"></code></pre>
|
|
192
|
+
</div>
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
// Cache references to various elements
|
|
196
|
+
this.baseStylesEl = getEl('#base-styles', this.shadowRoot) as HTMLStyleElement;
|
|
197
|
+
this.themeStylesEl = getEl('#theme-styles', this.shadowRoot) as HTMLStyleElement;
|
|
198
|
+
this.dynamicStylesEl = getEl('#dynamic-styles', this.shadowRoot) as HTMLStyleElement;
|
|
199
|
+
this.headerEl = getEl('#code-header', this.shadowRoot) as HTMLElement;
|
|
200
|
+
this.codeNameEl = getEl('#code-name', this.shadowRoot) as HTMLElement;
|
|
201
|
+
this.codeToolsEl = getEl('#code-tools', this.shadowRoot) as HTMLElement;
|
|
202
|
+
this.codeBodyEl = getEl('#code-body', this.shadowRoot) as HTMLElement;
|
|
203
|
+
this.highlightedCodeEl = getEl('#highlight-code', this.shadowRoot) as HTMLElement;
|
|
204
|
+
|
|
205
|
+
this.codeBodyEl.addEventListener('scroll', () => {
|
|
206
|
+
let flag = this.codeBodyEl.scrollTop + this.codeBodyEl.clientHeight < this.codeBodyEl.scrollHeight;
|
|
207
|
+
// Check if the user manually scrolled
|
|
208
|
+
this.autoScroll = !flag;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Registers a new language with a set of syntax highlighting rules.
|
|
213
|
+
* @param name - The name of the programming language (e.g., 'javascript', 'html', etc.)
|
|
214
|
+
* @param config - Configuration for the language, including rules, theme, and optional CSS.
|
|
215
|
+
*/
|
|
216
|
+
static register(name: string, config: LanguageConfig): void {
|
|
217
|
+
// Store the language configuration in the static map
|
|
218
|
+
this.languages.set(name, { ...config });
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Registers tools that can be used with the code editor (e.g., copy, download, etc.).
|
|
222
|
+
* @param items - An array of tool items to register.
|
|
223
|
+
*/
|
|
224
|
+
static addTools(items: toolsItem[]): void {
|
|
225
|
+
Coax.tools = items;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Mounts the tools to the code tools container.
|
|
229
|
+
* @param toolItems - An array of tool items to be added to the tools container.
|
|
230
|
+
*/
|
|
231
|
+
mountTools(toolItems: any[]) {
|
|
232
|
+
requestAnimationFrame(() => {
|
|
233
|
+
this.codeToolsEl.innerHTML = '';
|
|
234
|
+
let items = toolItems.map(k => {
|
|
235
|
+
k.action = k.action.bind(this);
|
|
236
|
+
return k;
|
|
237
|
+
});
|
|
238
|
+
this.codeToolsEl.appendChild(createTools(items));
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Called when the element is connected to the DOM.
|
|
243
|
+
*/
|
|
244
|
+
connectedCallback() {
|
|
245
|
+
this.render();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Observed attributes for changes. These include the language, height, tools, and speed.
|
|
250
|
+
*/
|
|
251
|
+
static get observedAttributes() { return ['lang', 'height', 'max-height', 'tools', 'speed']; }
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Called when any of the observed attributes change.
|
|
255
|
+
* @param name - The name of the changed attribute.
|
|
256
|
+
* @param oldVal - The old value of the attribute.
|
|
257
|
+
* @param newVal - The new value of the attribute.
|
|
258
|
+
*/
|
|
259
|
+
attributeChangedCallback(name: string, oldVal: string, newVal: string) {
|
|
260
|
+
if (oldVal === newVal) return;
|
|
261
|
+
if (name === 'height' || name === 'max-height') {
|
|
262
|
+
this.updateStyleByRegExp(name, newVal);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (name === 'speed') {
|
|
266
|
+
// Convert to integer (0 or 1)
|
|
267
|
+
this.speed = ~~(!!newVal);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (name === 'lang') {
|
|
271
|
+
// Update the language and re-render
|
|
272
|
+
this.lang = newVal;
|
|
273
|
+
this.render();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (name === 'tools') {
|
|
277
|
+
if (!newVal) this.codeToolsEl.innerHTML = '';
|
|
278
|
+
const tools = parseClasses(newVal),
|
|
279
|
+
toolItems = Coax.tools.filter(k => tools.includes(k.name));
|
|
280
|
+
if (!toolItems.length) return;
|
|
281
|
+
this.mountTools(toolItems);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Updates the base style by replacing specific CSS properties using a regular expression.
|
|
286
|
+
* @param prop - The CSS property name to update (e.g., 'height', 'max-height').
|
|
287
|
+
* @param value - The new value for the property.
|
|
288
|
+
*/
|
|
289
|
+
private updateStyleByRegExp(prop: string, value: string) {
|
|
290
|
+
// 构建正则:匹配属性名后面跟着冒号,直到分号或换行
|
|
291
|
+
// 例如:height:\s*[^;]+;
|
|
292
|
+
const regex = new RegExp(`;\\n\\s*${prop}:\\s*[^;]+;`, 'g');
|
|
293
|
+
// 替换为新的属性值
|
|
294
|
+
this.baseStylesEl.textContent = this.baseStylesEl.textContent.replace(regex, `;\n${prop}: ${value};`);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Retrieves the CSS prefix for the language configuration.
|
|
298
|
+
* @param config - The language configuration object.
|
|
299
|
+
* @returns The CSS prefix.
|
|
300
|
+
*/
|
|
301
|
+
getCssPrefix(config: LanguageConfig) {
|
|
302
|
+
return (config?.cssPrefix || this.lang).replace(/[^a-zA-Z0-9_-]/g, '\\$&');
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Highlights a given string according to the language configuration.
|
|
306
|
+
* @param string - The string to highlight.
|
|
307
|
+
* @param config - The language configuration object.
|
|
308
|
+
* @returns The highlighted string with HTML spans.
|
|
309
|
+
*/
|
|
310
|
+
getHighLightString(string: string, config?: LanguageConfig) {
|
|
311
|
+
config = config || Coax.languages.get(this.lang);
|
|
312
|
+
if (!config) return string;
|
|
313
|
+
// 如果找到了配置,则进行高亮处理
|
|
314
|
+
const cssPrefix = this.getCssPrefix(config),
|
|
315
|
+
// 获取用于语法高亮的正则表达式
|
|
316
|
+
combinedRegex = new RegExp(config.rules.map((r: any) => `(${r.pattern.source})`).join('|'), 'g');
|
|
317
|
+
return string.replace(combinedRegex, (match, ...args) => {
|
|
318
|
+
const i = args.findIndex(val => val !== undefined);
|
|
319
|
+
return i !== -1 && config.rules[i] ? `<span class="${NAMESPACE}-${cssPrefix}-${config.rules[i].token}">${match}</span>` : match;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Creates a wrapper element for a line of code.
|
|
324
|
+
* @param index - The line index to assign.
|
|
325
|
+
* @param startIndex - The starting index for the line.
|
|
326
|
+
* @returns A div element wrapping the line of code.
|
|
327
|
+
*/
|
|
328
|
+
createLineWrap(index?: number, startIndex?: number) {
|
|
329
|
+
let dataIndex = 0;
|
|
330
|
+
if (index == null && startIndex == null) {
|
|
331
|
+
dataIndex = this.highlightedCodeEl.children.length;
|
|
332
|
+
} else {
|
|
333
|
+
const start = startIndex || this.highlightedCodeEl.children.length;
|
|
334
|
+
dataIndex = start + (index as number);
|
|
335
|
+
}
|
|
336
|
+
return createEl('div', { 'data-index': dataIndex }, '<div></div>');
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Fills a line of code with highlighted content.
|
|
340
|
+
* @param codeWrap - The element that will contain the highlighted line of code.
|
|
341
|
+
* @param line - The line of code to highlight.
|
|
342
|
+
* @param config - The language configuration object.
|
|
343
|
+
*/
|
|
344
|
+
getLineToFill(codeWrap: Element, line: string, config?: LanguageConfig) {
|
|
345
|
+
config = config || Coax.languages.get(this.lang);
|
|
346
|
+
let highlightedLine = this.getHighLightString(line, config);
|
|
347
|
+
// 将高亮后的内容填充到 div 中
|
|
348
|
+
codeWrap.innerHTML = highlightedLine;
|
|
349
|
+
};
|
|
350
|
+
/**
|
|
351
|
+
* Highlights new source code and appends it to the code body.
|
|
352
|
+
* @param newCode - The new source code to highlight and append.
|
|
353
|
+
*/
|
|
354
|
+
async highlight(newCode: string) {
|
|
355
|
+
const config = Coax.languages.get(this.lang),
|
|
356
|
+
startIndex = this.highlightedCodeEl.children.length,
|
|
357
|
+
// 将新源码按行分割
|
|
358
|
+
newCodeLines = newCode ? newCode.split('\n') : [];
|
|
359
|
+
//更新别名
|
|
360
|
+
this.updateName(config)
|
|
361
|
+
if (!newCodeLines.length) return true;
|
|
362
|
+
// 如果没有找到配置,则输出原始代码,并不进行高亮处理
|
|
363
|
+
for (let [index, lineString] of newCodeLines.entries()) {
|
|
364
|
+
//如果是空行则跳过
|
|
365
|
+
if (!lineString.trim() && this.hasAttribute('sanitized')) continue;
|
|
366
|
+
// 创建一个 div 包裹每一行
|
|
367
|
+
const lineWrap = this.createLineWrap(index, startIndex),
|
|
368
|
+
codeWrap = lineWrap.lastElementChild as Element;
|
|
369
|
+
//标记完成
|
|
370
|
+
(lineWrap as any).completed = true;
|
|
371
|
+
|
|
372
|
+
if (this.hasAttribute('speed')) {
|
|
373
|
+
this.highlightedCodeEl.appendChild(lineWrap);
|
|
374
|
+
//流式打字
|
|
375
|
+
await typeWriter(lineString, {
|
|
376
|
+
speed: this.speed,
|
|
377
|
+
onDuringType: (char, fullText) => {
|
|
378
|
+
codeWrap.innerHTML = fullText;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
this.getLineToFill(codeWrap, lineString, config);
|
|
382
|
+
} else {
|
|
383
|
+
//直接打出
|
|
384
|
+
this.getLineToFill(codeWrap, lineString, config);
|
|
385
|
+
//
|
|
386
|
+
this.highlightedCodeEl.appendChild(lineWrap);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
//滚动到最底部
|
|
390
|
+
this.autoScrollCode();
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Automatically scrolls the code body to the bottom.
|
|
396
|
+
*/
|
|
397
|
+
autoScrollCode() {
|
|
398
|
+
if (this.autoScroll) {
|
|
399
|
+
this.codeBodyEl.scrollTop = this.codeBodyEl.scrollHeight;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Injects the theme styles for syntax highlighting (light/dark modes).
|
|
404
|
+
*/
|
|
405
|
+
injectThemeStyles() {
|
|
406
|
+
const config = Coax.languages.get(this.lang);
|
|
407
|
+
if (!config) return;
|
|
408
|
+
|
|
409
|
+
// Get language name, fallback to 'js' if not provided
|
|
410
|
+
let cssPrefix = this.getCssPrefix(config),
|
|
411
|
+
//Generate dynamic CSS classes for each syntax rule
|
|
412
|
+
// 为 rules 中的每一个 name 生成类似 .hl-name { color: var(--ax-code-name); }
|
|
413
|
+
lightStyles = config.rules.map((rule: any) => `
|
|
414
|
+
.${NAMESPACE}-${cssPrefix}-${rule.token} { color: var(--${NAMESPACE}-${cssPrefix}-${rule.token}${rule.light ? ',' + rule.light : ''});}`).join('\n'),
|
|
415
|
+
darkStyles = '',
|
|
416
|
+
schemeStyles = '';
|
|
417
|
+
|
|
418
|
+
darkStyles += `:host([scheme="dark"]){`;
|
|
419
|
+
darkStyles += config.rules.map((rule: any) => `
|
|
420
|
+
${rule.light ? `
|
|
421
|
+
.${NAMESPACE}-${cssPrefix}-${rule.token} {color: var(--${NAMESPACE}-${cssPrefix}-${rule.token},${rule.dark});}` : ``}`).join('\n');
|
|
422
|
+
darkStyles += `}`;
|
|
423
|
+
schemeStyles = `@media (prefers-color-scheme: dark){
|
|
424
|
+
:host{
|
|
425
|
+
`;
|
|
426
|
+
schemeStyles += config.rules.map((rule: any) => `
|
|
427
|
+
${rule.light ? `
|
|
428
|
+
.${NAMESPACE}-${cssPrefix}-${rule.token} { color: var(--${NAMESPACE}-${cssPrefix}-${rule.token},${rule.dark}); }` : ``} `).join('\n');
|
|
429
|
+
schemeStyles += `}`;
|
|
430
|
+
// Set the inner HTML of the shadow root, including styles and highlighted code
|
|
431
|
+
this.dynamicStylesEl.textContent = lightStyles + darkStyles + schemeStyles;
|
|
432
|
+
|
|
433
|
+
//附加主题样式
|
|
434
|
+
if (config?.themeStyles) {
|
|
435
|
+
this.themeStylesEl.textContent = config.themeStyles;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Updates the alias name for the language based on the configuration.
|
|
442
|
+
* @param config - The language configuration object.
|
|
443
|
+
*/
|
|
444
|
+
updateName(config: any) {
|
|
445
|
+
if (this.hasAttribute('unnamed')) return;
|
|
446
|
+
if (!config) {
|
|
447
|
+
this.codeNameEl.innerHTML = 'Plain Text';
|
|
448
|
+
} else {
|
|
449
|
+
//更新别名
|
|
450
|
+
this.alias = config.alias || this.lang;
|
|
451
|
+
this.codeNameEl.innerHTML = this.alias;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Renders the highlighted code within the shadow DOM.
|
|
456
|
+
*/
|
|
457
|
+
render(code = this.source) {
|
|
458
|
+
//同时多次改变属性,只执行一次
|
|
459
|
+
if (this._renderQueued) return;
|
|
460
|
+
this._renderQueued = true;
|
|
461
|
+
// 使用 requestAnimationFrame 将渲染推迟到下一帧
|
|
462
|
+
requestAnimationFrame(async () => {
|
|
463
|
+
this.clear();
|
|
464
|
+
this.injectThemeStyles();
|
|
465
|
+
//一次性渲染
|
|
466
|
+
await this.highlight(code);
|
|
467
|
+
this._renderQueued = false;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Clears the current content and resets the state.
|
|
472
|
+
*/
|
|
473
|
+
clear() {
|
|
474
|
+
this.highlightedCodeEl.innerHTML = this.source = this.lineString = '';
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Replaces the existing code with new source code and re-renders.
|
|
478
|
+
* @param newCode - The new source code to replace the existing code.
|
|
479
|
+
*/
|
|
480
|
+
async replace(newCode: string) {
|
|
481
|
+
this.clear();
|
|
482
|
+
await this.highlight(newCode);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Appends new source code to the current content and highlights only the new portion.
|
|
486
|
+
* @param newCode - The new source code to append and highlight.
|
|
487
|
+
*/
|
|
488
|
+
async append(newCode: string) {
|
|
489
|
+
// 将新的代码追加到现有代码末尾
|
|
490
|
+
this.source += `\n${newCode}`;
|
|
491
|
+
// 高亮新的部分
|
|
492
|
+
await this.highlight(newCode);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Retrieves the last line of code that was rendered.
|
|
496
|
+
* @returns An object containing the line wrapper and code wrapper for the last line.
|
|
497
|
+
*/
|
|
498
|
+
getLastLine() {
|
|
499
|
+
const lastChild = this.highlightedCodeEl.lastElementChild,
|
|
500
|
+
lastLine = !lastChild || (this.highlightedCodeEl.lastElementChild as any)?.completed ?
|
|
501
|
+
this.createLineWrap() : lastChild;
|
|
502
|
+
return {
|
|
503
|
+
lineWrap: lastLine,
|
|
504
|
+
codeWrap: lastLine.lastElementChild as Element
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Marks the current line as completed and updates the displayed code.
|
|
509
|
+
*/
|
|
510
|
+
close() {
|
|
511
|
+
const lineWrap = this.highlightedCodeEl.lastElementChild;
|
|
512
|
+
if (!lineWrap) return;
|
|
513
|
+
(lineWrap as any).completed = true;
|
|
514
|
+
//行结束前保存
|
|
515
|
+
this.lastLineString = this.lineString;
|
|
516
|
+
this.getLineToFill(lineWrap?.lastElementChild as Element, this.lineString);
|
|
517
|
+
//一行结束,清空临时行文本
|
|
518
|
+
this.lineString = '';
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Reopens the last closed line and restores its original content.
|
|
522
|
+
*/
|
|
523
|
+
open() {
|
|
524
|
+
const lineWrap = this.highlightedCodeEl.lastElementChild;
|
|
525
|
+
if (!lineWrap) return;
|
|
526
|
+
(lineWrap as any).completed = false;
|
|
527
|
+
//恢复最后一行字符串
|
|
528
|
+
(lineWrap?.lastElementChild as any).textContent = this.lineString = this.lastLineString;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Streams a string of code into the component, either appending or closing the current line.
|
|
532
|
+
* @param str - The code string to stream into the component.
|
|
533
|
+
* @param forceClose - Forcefully close the line if set to `true`.
|
|
534
|
+
*/
|
|
535
|
+
stream(str: string, forceClose: boolean = false) {
|
|
536
|
+
const { lineWrap, codeWrap } = this.getLastLine();
|
|
537
|
+
this.highlightedCodeEl.appendChild(lineWrap);
|
|
538
|
+
//汇集
|
|
539
|
+
this.source += str;
|
|
540
|
+
|
|
541
|
+
//如果没有遇到换行符号,也可以强制结束
|
|
542
|
+
if (forceClose || (str.startsWith('\n') || str.endsWith('\n'))) {
|
|
543
|
+
//标记完成
|
|
544
|
+
this.close();
|
|
545
|
+
} else {
|
|
546
|
+
//插入文本
|
|
547
|
+
codeWrap.innerHTML += str;
|
|
548
|
+
//临时保存行文本
|
|
549
|
+
this.lineString += str;
|
|
550
|
+
}
|
|
551
|
+
//滚动到最底部
|
|
552
|
+
this.autoScrollCode();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export default Coax;
|
package/src/modules.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since Last modified: 2026/01/12 14:10:29
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
import Coax from './components/Coax.js';
|
|
6
|
+
import css from './rules/css.js';
|
|
7
|
+
import html from './rules/html.js';
|
|
8
|
+
import javascript from './rules/javascript.js';
|
|
9
|
+
import markdown from './rules/markdown.js';
|
|
10
|
+
import typescript from './rules/typescript.js';
|
|
11
|
+
import copy from './tools/copy.js';
|
|
12
|
+
export { Coax, copy, css, html, javascript, typescript, markdown, };
|
package/src/modules.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since Last modified: 2026/01/12 14:10:29
|
|
3
|
+
*/
|
|
4
|
+
'use strict'
|
|
5
|
+
|
|
6
|
+
import Coax from './components/Coax.js';
|
|
7
|
+
import css from './rules/css.js';
|
|
8
|
+
import html from './rules/html.js';
|
|
9
|
+
import javascript from './rules/javascript.js';
|
|
10
|
+
import markdown from './rules/markdown.js';
|
|
11
|
+
import typescript from './rules/typescript.js';
|
|
12
|
+
import copy from './tools/copy.js';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
Coax,
|
|
16
|
+
copy,
|
|
17
|
+
css,
|
|
18
|
+
html,
|
|
19
|
+
javascript,
|
|
20
|
+
typescript,
|
|
21
|
+
markdown,
|
|
22
|
+
|
|
23
|
+
};
|
package/src/rules/css.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const css = [
|
|
2
|
+
{ token: 'comment', pattern: /\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
|
|
3
|
+
{ token: 'value', pattern: /(?:'|")(?:\\.|[^\\'"\b])*?(?:'|")/, light: '#032f62', dark: '#a5d6ff' },
|
|
4
|
+
{ token: 'func', pattern: /[a-z-]+\(?=/, light: '#e36209', dark: '#ffa657' },
|
|
5
|
+
{ token: 'property', pattern: /[a-z-]+(?=\s*:)/, light: '#005cc5', dark: '#79c0ff' },
|
|
6
|
+
{ token: 'selector', pattern: /[.#a-z0-9, \n\t>:+()_-]+(?=\s*\{)/i, light: '#22863a', dark: '#7ee787' },
|
|
7
|
+
{ token: 'unit', pattern: /(?<=\d)(px|em|rem|%|vh|vw|ms|s|deg)/, light: '#d73a49', dark: '#ff7b72' },
|
|
8
|
+
{ token: 'number', pattern: /\b\d+(\.\d+)?\b/, light: '#005cc5', dark: '#79c0ff' },
|
|
9
|
+
{ token: 'punct', pattern: /[{}();:]/, light: '#24292e', dark: '#c9d1d9' }
|
|
10
|
+
];
|
|
11
|
+
export default css;
|
package/src/rules/css.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const css = [
|
|
2
|
+
{ token: 'comment', pattern: /\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
|
|
3
|
+
{ token: 'value', pattern: /(?:'|")(?:\\.|[^\\'"\b])*?(?:'|")/, light: '#032f62', dark: '#a5d6ff' },
|
|
4
|
+
{ token: 'func', pattern: /[a-z-]+\(?=/, light: '#e36209', dark: '#ffa657' },
|
|
5
|
+
{ token: 'property', pattern: /[a-z-]+(?=\s*:)/, light: '#005cc5', dark: '#79c0ff' },
|
|
6
|
+
{ token: 'selector', pattern: /[.#a-z0-9, \n\t>:+()_-]+(?=\s*\{)/i, light: '#22863a', dark: '#7ee787' },
|
|
7
|
+
{ token: 'unit', pattern: /(?<=\d)(px|em|rem|%|vh|vw|ms|s|deg)/, light: '#d73a49', dark: '#ff7b72' },
|
|
8
|
+
{ token: 'number', pattern: /\b\d+(\.\d+)?\b/, light: '#005cc5', dark: '#79c0ff' },
|
|
9
|
+
{ token: 'punct', pattern: /[{}();:]/, light: '#24292e', dark: '#c9d1d9' }
|
|
10
|
+
]
|
|
11
|
+
export default css;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const html = [
|
|
2
|
+
{ token: 'comment', pattern: /<!--[\s\S]*?-->/, light: '#999999', dark: '#6e7681' },
|
|
3
|
+
{ token: 'doctype', pattern: /<!DOCTYPE[\s\S]*?>/i, light: '#6a737d', dark: '#8b949e' },
|
|
4
|
+
// 匹配标签名: <div, </div
|
|
5
|
+
{ token: 'tag', pattern: /<\/?[a-zA-Z0-9]+/, light: '#22863a', dark: '#7ee787' },
|
|
6
|
+
// 匹配属性名: class=
|
|
7
|
+
{ token: 'attr', pattern: /[a-zA-Z-]+(?=\s*=\s*)/, light: '#6f42c1', dark: '#d2a8ff' },
|
|
8
|
+
// 匹配属性值: "value"
|
|
9
|
+
{ token: 'string', pattern: /(['"])(?:\\.|[^\\])*?\1/, light: '#032f62', dark: '#a5d6ff' },
|
|
10
|
+
// 匹配标签闭合: >, />
|
|
11
|
+
{ token: 'bracket', pattern: /\/?>/, light: '#24292e', dark: '#c9d1d9' }
|
|
12
|
+
];
|
|
13
|
+
export default html;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const html = [
|
|
2
|
+
{ token: 'comment', pattern: /<!--[\s\S]*?-->/, light: '#999999', dark: '#6e7681' },
|
|
3
|
+
{ token: 'doctype', pattern: /<!DOCTYPE[\s\S]*?>/i, light: '#6a737d', dark: '#8b949e' },
|
|
4
|
+
// 匹配标签名: <div, </div
|
|
5
|
+
{ token: 'tag', pattern: /<\/?[a-zA-Z0-9]+/, light: '#22863a', dark: '#7ee787' },
|
|
6
|
+
// 匹配属性名: class=
|
|
7
|
+
{ token: 'attr', pattern: /[a-zA-Z-]+(?=\s*=\s*)/, light: '#6f42c1', dark: '#d2a8ff' },
|
|
8
|
+
// 匹配属性值: "value"
|
|
9
|
+
{ token: 'string', pattern: /(['"])(?:\\.|[^\\])*?\1/, light: '#032f62', dark: '#a5d6ff' },
|
|
10
|
+
// 匹配标签闭合: >, />
|
|
11
|
+
{ token: 'bracket', pattern: /\/?>/, light: '#24292e', dark: '#c9d1d9' }
|
|
12
|
+
]
|
|
13
|
+
export default html;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const javascript = [
|
|
2
|
+
{ token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
|
|
3
|
+
{ token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, light: '#032f62', dark: '#98c379' },
|
|
4
|
+
{ 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' },
|
|
5
|
+
{ token: 'builtin', pattern: /\b(console|window|document|Math|JSON|true|false|null|undefined|Object|Array|Promise|Number|String|Boolean)\b/, light: '#e36209', dark: '#ffa657' },
|
|
6
|
+
{ token: 'number', pattern: /\b(0x[\da-fA-F]+|0b[01]+|\d+(\.\d+)?)\b/, light: '#005cc5', dark: '#79c0ff' },
|
|
7
|
+
{ token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, light: '#6f42c1', dark: '#d2a8ff' },
|
|
8
|
+
{ token: 'op', pattern: /[+\-*/%=<>!&|^~]+/, light: '#069598', dark: '#56b6c2' }
|
|
9
|
+
];
|
|
10
|
+
export default javascript;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const javascript = [
|
|
2
|
+
{ token: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\//, light: '#6a737d', dark: '#8b949e' },
|
|
3
|
+
{ token: 'string', pattern: /(?:'|"|`)(?:\\.|[^\\'"\b])*?(?:'|"|`)/, light: '#032f62', dark: '#98c379' },
|
|
4
|
+
{ 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' },
|
|
5
|
+
{ token: 'builtin', pattern: /\b(console|window|document|Math|JSON|true|false|null|undefined|Object|Array|Promise|Number|String|Boolean)\b/, light: '#e36209', dark: '#ffa657' },
|
|
6
|
+
{ token: 'number', pattern: /\b(0x[\da-fA-F]+|0b[01]+|\d+(\.\d+)?)\b/, light: '#005cc5', dark: '#79c0ff' },
|
|
7
|
+
{ token: 'func', pattern: /\b[a-zA-Z_]\w*(?=\s*\()/, light: '#6f42c1', dark: '#d2a8ff' },
|
|
8
|
+
{ token: 'op', pattern: /[+\-*/%=<>!&|^~]+/, light: '#069598', dark: '#56b6c2' }
|
|
9
|
+
]
|
|
10
|
+
export default javascript;
|