@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.
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
+ };