@kissmybutton/mc-code 0.0.1
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/.babelrc +14 -0
- package/.eslintignore +2 -0
- package/.eslintrc +69 -0
- package/README.md +121 -0
- package/dist/bundle.cjs.js +365 -0
- package/dist/bundle.esm.js +363 -0
- package/dist/bundle.umd.js +1 -0
- package/package.json +87 -0
- package/renovate.json +10 -0
- package/rollup.config.js +67 -0
- package/src/Incidents/CodeClip.js +237 -0
- package/src/Incidents/CodeHighlight.js +53 -0
- package/src/Incidents/WriteCode.js +44 -0
- package/src/index.js +22 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { BrowserClip } from "@donkeyclip/motorcortex";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CodeClip — a BrowserClip that renders syntax-highlighted code.
|
|
5
|
+
*
|
|
6
|
+
* Each line gets id: line_0, line_1, ... for targeting.
|
|
7
|
+
* Registers custom entity "code" for Effects via !#code.
|
|
8
|
+
*
|
|
9
|
+
* Attrs:
|
|
10
|
+
* code: string — the source code to display
|
|
11
|
+
* language: string (default: "javascript")
|
|
12
|
+
* theme: "dark" | "light" (default: "dark")
|
|
13
|
+
* fontSize: number (default: 14)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const THEMES = {
|
|
17
|
+
dark: { bg: "#1e1e2e", fg: "#cdd6f4", lineNum: "#585b70", keyword: "#cba6f7", string: "#a6e3a1", comment: "#6c7086", fn: "#89b4fa", number: "#fab387", punct: "#94e2d5", type: "#f9e2af" },
|
|
18
|
+
light: { bg: "#fafafa", fg: "#383a42", lineNum: "#9ca0b0", keyword: "#a626a4", string: "#50a14f", comment: "#a0a1a7", fn: "#4078f2", number: "#986801", punct: "#0184bc", type: "#c18401" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const LANG_KEYWORDS = {
|
|
22
|
+
javascript: new Set(["const","let","var","function","return","if","else","for","while","do","switch","case","break","continue","new","this","class","extends","import","export","default","from","of","in","typeof","instanceof","try","catch","finally","throw","async","await","yield","null","undefined","true","false"]),
|
|
23
|
+
typescript: new Set(["const","let","var","function","return","if","else","for","while","do","switch","case","break","continue","new","this","class","extends","import","export","default","from","of","in","typeof","instanceof","try","catch","finally","throw","async","await","yield","null","undefined","true","false","type","interface","enum","implements","declare","readonly","as","is","keyof","never","unknown","any","void","abstract","override","satisfies","infer"]),
|
|
24
|
+
python: new Set(["def","return","if","elif","else","for","while","in","not","and","or","is","class","import","from","as","with","try","except","finally","raise","pass","break","continue","yield","lambda","None","True","False","self","print","range","len","dict","list","set","tuple","int","str","float","bool","async","await"]),
|
|
25
|
+
bash: new Set(["if","then","else","elif","fi","for","do","done","while","until","case","esac","function","return","in","echo","exit","export","source","local","readonly","declare","set","unset","shift","cd","pwd","ls","grep","sed","awk","cat","mkdir","rm","cp","mv","chmod","chown","sudo","apt","yum","npm","pip","git","docker","kubectl"]),
|
|
26
|
+
html: new Set(["div","span","p","a","img","ul","ol","li","h1","h2","h3","h4","h5","h6","table","tr","td","th","form","input","button","select","option","textarea","label","header","footer","nav","main","section","article","aside","script","style","link","meta","head","body","html"]),
|
|
27
|
+
css: new Set(["color","background","border","margin","padding","display","position","width","height","font","text","flex","grid","align","justify","overflow","opacity","transform","transition","animation","z-index","top","left","right","bottom","none","auto","inherit","initial","important"]),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// JSON/YAML don't have keywords in the traditional sense — handled by tokenizer rules
|
|
31
|
+
const COMMENT_STYLES = {
|
|
32
|
+
javascript: "//", typescript: "//", python: "#", bash: "#", yaml: "#",
|
|
33
|
+
css: null, json: null, html: null,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function escapeHtml(s) {
|
|
37
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tokenizeLine(line, theme, language) {
|
|
41
|
+
const t = theme;
|
|
42
|
+
const keywords = LANG_KEYWORDS[language] || LANG_KEYWORDS.javascript;
|
|
43
|
+
const commentChar = COMMENT_STYLES[language] ?? "//";
|
|
44
|
+
let result = "";
|
|
45
|
+
let i = 0;
|
|
46
|
+
|
|
47
|
+
// YAML/JSON: color keys specially
|
|
48
|
+
if (language === "yaml") {
|
|
49
|
+
// YAML line: "key: value" or "- item"
|
|
50
|
+
const yamlKeyMatch = line.match(/^(\s*)([\w.\-/]+)(\s*:\s*)(.*)/);
|
|
51
|
+
if (yamlKeyMatch) {
|
|
52
|
+
const [, indent, key, colon, value] = yamlKeyMatch;
|
|
53
|
+
result += escapeHtml(indent);
|
|
54
|
+
result += `<span style="color:${t.fn}">${escapeHtml(key)}</span>`;
|
|
55
|
+
result += `<span style="color:${t.punct}">${escapeHtml(colon)}</span>`;
|
|
56
|
+
// Value coloring
|
|
57
|
+
if (/^(true|false|null|~)$/i.test(value.trim())) {
|
|
58
|
+
result += `<span style="color:${t.keyword};font-weight:600">${escapeHtml(value)}</span>`;
|
|
59
|
+
} else if (/^\d+(\.\d+)?$/.test(value.trim())) {
|
|
60
|
+
result += `<span style="color:${t.number}">${escapeHtml(value)}</span>`;
|
|
61
|
+
} else if (/^["']/.test(value.trim())) {
|
|
62
|
+
result += `<span style="color:${t.string}">${escapeHtml(value)}</span>`;
|
|
63
|
+
} else {
|
|
64
|
+
result += `<span style="color:${t.fg}">${escapeHtml(value)}</span>`;
|
|
65
|
+
}
|
|
66
|
+
return result || " ";
|
|
67
|
+
}
|
|
68
|
+
const yamlDash = line.match(/^(\s*)(- )(.*)/);
|
|
69
|
+
if (yamlDash) {
|
|
70
|
+
result += escapeHtml(yamlDash[1]);
|
|
71
|
+
result += `<span style="color:${t.punct}">${escapeHtml(yamlDash[2])}</span>`;
|
|
72
|
+
result += `<span style="color:${t.fg}">${escapeHtml(yamlDash[3])}</span>`;
|
|
73
|
+
return result || " ";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (language === "json") {
|
|
78
|
+
// JSON: color keys in quotes before colon
|
|
79
|
+
const jsonKeyMatch = line.match(/^(\s*)("(?:[^"\\]|\\.)*")(\s*:\s*)(.*)/);
|
|
80
|
+
if (jsonKeyMatch) {
|
|
81
|
+
const [, indent, key, colon, value] = jsonKeyMatch;
|
|
82
|
+
result += escapeHtml(indent);
|
|
83
|
+
result += `<span style="color:${t.fn}">${escapeHtml(key)}</span>`;
|
|
84
|
+
result += `<span style="color:${t.punct}">${escapeHtml(colon)}</span>`;
|
|
85
|
+
if (/^(true|false|null)/.test(value.trim())) {
|
|
86
|
+
result += `<span style="color:${t.keyword};font-weight:600">${escapeHtml(value)}</span>`;
|
|
87
|
+
} else if (/^\d/.test(value.trim())) {
|
|
88
|
+
result += `<span style="color:${t.number}">${escapeHtml(value)}</span>`;
|
|
89
|
+
} else if (/^"/.test(value.trim())) {
|
|
90
|
+
result += `<span style="color:${t.string}">${escapeHtml(value)}</span>`;
|
|
91
|
+
} else {
|
|
92
|
+
result += `<span style="color:${t.fg}">${escapeHtml(value)}</span>`;
|
|
93
|
+
}
|
|
94
|
+
return result || " ";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
while (i < line.length) {
|
|
99
|
+
// Comments: // or # to end of line
|
|
100
|
+
if (commentChar && line.startsWith(commentChar, i) && (i === 0 || /\s/.test(line[i - 1]) || i === 0)) {
|
|
101
|
+
result += `<span style="color:${t.comment};font-style:italic">${escapeHtml(line.slice(i))}</span>`;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Strings: single or double quoted
|
|
106
|
+
if (line[i] === '"' || line[i] === "'" || line[i] === "`") {
|
|
107
|
+
const q = line[i];
|
|
108
|
+
let j = i + 1;
|
|
109
|
+
while (j < line.length && line[j] !== q) { if (line[j] === "\\") j++; j++; }
|
|
110
|
+
j = Math.min(j + 1, line.length);
|
|
111
|
+
result += `<span style="color:${t.string}">${escapeHtml(line.slice(i, j))}</span>`;
|
|
112
|
+
i = j;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Numbers
|
|
117
|
+
if (/\d/.test(line[i]) && (i === 0 || /[\s,;(=+\-*/<>!&|]/.test(line[i - 1]))) {
|
|
118
|
+
let j = i;
|
|
119
|
+
while (j < line.length && /[\d.]/.test(line[j])) j++;
|
|
120
|
+
result += `<span style="color:${t.number}">${escapeHtml(line.slice(i, j))}</span>`;
|
|
121
|
+
i = j;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Words (keywords, identifiers)
|
|
126
|
+
if (/[a-zA-Z_$]/.test(line[i])) {
|
|
127
|
+
let j = i;
|
|
128
|
+
while (j < line.length && /[a-zA-Z0-9_$]/.test(line[j])) j++;
|
|
129
|
+
const word = line.slice(i, j);
|
|
130
|
+
if (keywords.has(word)) {
|
|
131
|
+
result += `<span style="color:${t.keyword};font-weight:600">${escapeHtml(word)}</span>`;
|
|
132
|
+
} else if (j < line.length && line[j] === "(") {
|
|
133
|
+
result += `<span style="color:${t.fn}">${escapeHtml(word)}</span>`;
|
|
134
|
+
} else {
|
|
135
|
+
result += `<span style="color:${t.fg}">${escapeHtml(word)}</span>`;
|
|
136
|
+
}
|
|
137
|
+
i = j;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Punctuation / operators
|
|
142
|
+
if (/[{}()\[\];:.,=+\-*/<>!&|?%^~@]/.test(line[i])) {
|
|
143
|
+
result += `<span style="color:${t.punct}">${escapeHtml(line[i])}</span>`;
|
|
144
|
+
i++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Whitespace and anything else
|
|
149
|
+
result += escapeHtml(line[i]);
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result || " ";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderLines(code, theme, language) {
|
|
157
|
+
const lines = code.split("\n");
|
|
158
|
+
return lines.map((line, i) =>
|
|
159
|
+
`<div class="shiki-line" id="line_${i}"><span class="line-num">${i + 1}</span><span class="line-code">${tokenizeLine(line, theme, language)}</span></div>`
|
|
160
|
+
).join("");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default class CodeClip extends BrowserClip {
|
|
164
|
+
get html() {
|
|
165
|
+
const code = this.attrs.code || "";
|
|
166
|
+
const language = this.attrs.language || "javascript";
|
|
167
|
+
const t = THEMES[this.attrs.theme] || THEMES.dark;
|
|
168
|
+
const linesHtml = renderLines(code, t, language);
|
|
169
|
+
return `<div id="code-container"><pre><code>${linesHtml}</code></pre></div>`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get css() {
|
|
173
|
+
const fs = this.attrs.fontSize || 14;
|
|
174
|
+
const t = THEMES[this.attrs.theme] || THEMES.dark;
|
|
175
|
+
return `
|
|
176
|
+
#code-container {
|
|
177
|
+
width: 100%;
|
|
178
|
+
height: 100%;
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
justify-content: center;
|
|
182
|
+
padding: 24px;
|
|
183
|
+
box-sizing: border-box;
|
|
184
|
+
background: ${t.bg};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
pre {
|
|
188
|
+
margin: 0;
|
|
189
|
+
padding: 20px 24px;
|
|
190
|
+
border-radius: 10px;
|
|
191
|
+
background: ${t.bg};
|
|
192
|
+
font-size: ${fs}px;
|
|
193
|
+
line-height: 1.7;
|
|
194
|
+
overflow-x: auto;
|
|
195
|
+
width: 100%;
|
|
196
|
+
max-height: 100%;
|
|
197
|
+
box-sizing: border-box;
|
|
198
|
+
font-family: "Fira Code", "Cascadia Code", "JetBrains Mono", "Consolas", monospace;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
code {
|
|
202
|
+
color: ${t.fg};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.shiki-line {
|
|
206
|
+
display: flex;
|
|
207
|
+
padding: 1px 8px;
|
|
208
|
+
border-radius: 4px;
|
|
209
|
+
gap: 16px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.line-num {
|
|
213
|
+
color: ${t.lineNum};
|
|
214
|
+
user-select: none;
|
|
215
|
+
min-width: 2em;
|
|
216
|
+
text-align: right;
|
|
217
|
+
flex-shrink: 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.line-code {
|
|
221
|
+
white-space: pre;
|
|
222
|
+
}
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
onAfterRender() {
|
|
227
|
+
const code = this.attrs.code || "";
|
|
228
|
+
const container = this.context.getElements("#code-container")[0];
|
|
229
|
+
const lines = container.querySelectorAll(".shiki-line");
|
|
230
|
+
|
|
231
|
+
this.setCustomEntity("code", {
|
|
232
|
+
container,
|
|
233
|
+
lines,
|
|
234
|
+
lineCount: lines.length,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Effect } from "@donkeyclip/motorcortex";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CodeHighlight — Effect that pulses a line's background color.
|
|
5
|
+
*
|
|
6
|
+
* Selector: !#code
|
|
7
|
+
* animatedAttrs: { highlightLine_N: 1 } — where N is the line index (0-based)
|
|
8
|
+
* The line index is parsed from the attr key.
|
|
9
|
+
*
|
|
10
|
+
* Attrs:
|
|
11
|
+
* color: string (default: "rgba(99,102,241,0.25)") — highlight background
|
|
12
|
+
*/
|
|
13
|
+
export default class CodeHighlight extends Effect {
|
|
14
|
+
getScratchValue() {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
onGetContext() {
|
|
19
|
+
this._lines = this.element?.entity?.lines;
|
|
20
|
+
this._color = this.attrs.color || "rgba(99,102,241,0.25)";
|
|
21
|
+
// Parse line index from attributeKey: "highlightLine_4" → 4
|
|
22
|
+
const key = this.attributeKey || "";
|
|
23
|
+
this._lineIndex = parseInt(key.split("_").pop(), 10);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onProgress(millisecond) {
|
|
27
|
+
if (!this._lines || isNaN(this._lineIndex) || this._lineIndex < 0 || this._lineIndex >= this._lines.length) return;
|
|
28
|
+
const fraction = this.getFraction(millisecond);
|
|
29
|
+
const line = this._lines[this._lineIndex];
|
|
30
|
+
|
|
31
|
+
// Double pump: 0→0.25 up, 0.25→0.5 down, 0.5→0.75 up, 0.75→1 down
|
|
32
|
+
let intensity;
|
|
33
|
+
if (fraction < 0.25) {
|
|
34
|
+
intensity = fraction / 0.25;
|
|
35
|
+
} else if (fraction < 0.5) {
|
|
36
|
+
intensity = 1 - (fraction - 0.25) / 0.25;
|
|
37
|
+
} else if (fraction < 0.75) {
|
|
38
|
+
intensity = (fraction - 0.5) / 0.25;
|
|
39
|
+
} else {
|
|
40
|
+
intensity = 1 - (fraction - 0.75) / 0.25;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (intensity > 0.01) {
|
|
44
|
+
const alpha = intensity * 0.35;
|
|
45
|
+
const baseRgb = this._color.match(/\d+/g);
|
|
46
|
+
if (baseRgb && baseRgb.length >= 3) {
|
|
47
|
+
line.style.backgroundColor = `rgba(${baseRgb[0]},${baseRgb[1]},${baseRgb[2]},${alpha})`;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
line.style.backgroundColor = "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Effect } from "@donkeyclip/motorcortex";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WriteCode — Effect that progressively reveals code lines.
|
|
5
|
+
*
|
|
6
|
+
* Selector: !#code
|
|
7
|
+
* animatedAttrs: { writeCode: 1 } — fraction 0→1 reveals all lines
|
|
8
|
+
*/
|
|
9
|
+
export default class WriteCode extends Effect {
|
|
10
|
+
getScratchValue() {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
onGetContext() {
|
|
15
|
+
this._lines = this.element?.entity?.lines;
|
|
16
|
+
if (this._lines) {
|
|
17
|
+
// Hide all lines initially
|
|
18
|
+
for (const line of this._lines) {
|
|
19
|
+
line.style.opacity = "0";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
onProgress(millisecond) {
|
|
25
|
+
if (!this._lines) return;
|
|
26
|
+
const fraction = this.getFraction(millisecond);
|
|
27
|
+
const initial = this.initialValue ?? 0;
|
|
28
|
+
const target = this.targetValue;
|
|
29
|
+
const current = initial + (target - initial) * fraction;
|
|
30
|
+
|
|
31
|
+
const total = this._lines.length;
|
|
32
|
+
const revealUpTo = current * total;
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < total; i++) {
|
|
35
|
+
if (i < Math.floor(revealUpTo)) {
|
|
36
|
+
this._lines[i].style.opacity = "1";
|
|
37
|
+
} else if (i < revealUpTo) {
|
|
38
|
+
this._lines[i].style.opacity = String(revealUpTo - i);
|
|
39
|
+
} else {
|
|
40
|
+
this._lines[i].style.opacity = "0";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import CodeClip from "./Incidents/CodeClip";
|
|
2
|
+
import CodeHighlight from "./Incidents/CodeHighlight";
|
|
3
|
+
import WriteCode from "./Incidents/WriteCode";
|
|
4
|
+
import { name, version } from "../package.json";
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
npm_name: name,
|
|
8
|
+
version: version,
|
|
9
|
+
incidents: [
|
|
10
|
+
{
|
|
11
|
+
exportable: CodeHighlight,
|
|
12
|
+
name: "CodeHighlight",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
exportable: WriteCode,
|
|
16
|
+
name: "WriteCode",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
Clip: {
|
|
20
|
+
exportable: CodeClip,
|
|
21
|
+
},
|
|
22
|
+
};
|