@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 ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "presets": [
3
+ [
4
+ "@babel/preset-env",
5
+ {
6
+ "targets": "defaults, not ie 11",
7
+ "corejs": "3.21"
8
+ }
9
+ ]
10
+ ],
11
+ "plugins": [
12
+ "@babel/plugin-syntax-import-assertions"
13
+ ]
14
+ }
package/.eslintignore ADDED
@@ -0,0 +1,2 @@
1
+ bundle.js
2
+ dist
package/.eslintrc ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "parser": "@babel/eslint-parser",
3
+ "parserOptions": {
4
+ "babelOptions": {
5
+ "presets": ["@babel/preset-react"]
6
+ }
7
+ },
8
+ "extends": [
9
+ "eslint:recommended",
10
+ "plugin:react/recommended",
11
+ "prettier"
12
+ ],
13
+ "plugins": [
14
+ "babel",
15
+ "prettier",
16
+ "react"
17
+ ],
18
+ "env": {
19
+ "browser": true,
20
+ "node": true
21
+ },
22
+ "globals": {
23
+ "document": false,
24
+ "escape": false,
25
+ "navigator": false,
26
+ "unescape": false,
27
+ "window": false,
28
+ "describe": true,
29
+ "before": true,
30
+ "it": true,
31
+ "expect": true,
32
+ "sinon": true
33
+ },
34
+ "rules": {
35
+ "prettier/prettier": [
36
+ "error"
37
+ ],
38
+ "linebreak-style": [
39
+ "error",
40
+ "unix"
41
+ ],
42
+ "semi": [
43
+ "error",
44
+ "always"
45
+ ],
46
+ "no-console": [
47
+ "error",
48
+ {
49
+ "allow": [
50
+ "warn",
51
+ "error",
52
+ "info"
53
+ ]
54
+ }
55
+ ],
56
+ "prefer-promise-reject-errors": "error",
57
+ "prefer-const": [
58
+ "error",
59
+ {
60
+ "destructuring": "any",
61
+ "ignoreReadBeforeAssign": false
62
+ }
63
+ ],
64
+ "no-var": "error",
65
+ "no-unused-vars": "error",
66
+ "react/react-in-jsx-scope": "off",
67
+ "react/jsx-uses-react": "off"
68
+ }
69
+ }
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @kissmybutton/mc-code
2
+
3
+ MotorCortex plugin for displaying syntax-highlighted code with line-level animation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @kissmybutton/mc-code
9
+ ```
10
+
11
+ Peer dependency: `@donkeyclip/motorcortex >= 9.24.0`
12
+
13
+ ## Supported languages
14
+
15
+ JavaScript, TypeScript, Python, Bash, JSON, YAML, HTML, CSS. Each with language-specific keyword coloring, comment styles, and structural highlighting.
16
+
17
+ ## Quick start
18
+
19
+ ```js
20
+ import { HTMLClip, loadPlugin } from "@donkeyclip/motorcortex";
21
+ import Player from "@donkeyclip/motorcortex-player";
22
+ import codeDef from "@kissmybutton/mc-code";
23
+
24
+ const McCode = loadPlugin(codeDef);
25
+
26
+ const clip = new HTMLClip({
27
+ html: '<div id="scene" style="width:100%;height:100%;"></div>',
28
+ host: document.getElementById("clip"),
29
+ containerParams: { width: "900px", height: "600px" },
30
+ });
31
+
32
+ const codeClip = new McCode.Clip(
33
+ {
34
+ code: 'function hello() {\n return "world";\n}',
35
+ language: "javascript",
36
+ theme: "dark",
37
+ fontSize: 15,
38
+ },
39
+ {
40
+ selector: "#scene",
41
+ containerParams: { width: "900px", height: "600px" },
42
+ },
43
+ );
44
+
45
+ // Progressive write-in
46
+ codeClip.addIncident(
47
+ new McCode.WriteCode(
48
+ { animatedAttrs: { writeCode: 1 } },
49
+ { selector: "!#code", duration: 2000 },
50
+ ),
51
+ 500,
52
+ );
53
+
54
+ // Highlight line 1
55
+ codeClip.addIncident(
56
+ new McCode.CodeHighlight(
57
+ { animatedAttrs: { highlightLine_1: 1 }, color: "rgba(99,102,241,0.35)" },
58
+ { selector: "!#code", duration: 800 },
59
+ ),
60
+ 3000,
61
+ );
62
+
63
+ clip.addIncident(codeClip, 0);
64
+ new Player({ clip });
65
+ ```
66
+
67
+ ## Clip attrs
68
+
69
+ | Attr | Type | Default | Description |
70
+ | ---------- | ------ | -------------- | ------------------------------------ |
71
+ | `code` | string | `""` | The source code to display |
72
+ | `language` | string | `"javascript"` | Language for syntax coloring |
73
+ | `theme` | string | `"dark"` | Color theme: `"dark"` or `"light"` |
74
+ | `fontSize` | number | `14` | Font size in pixels |
75
+
76
+ ## Incidents
77
+
78
+ ### WriteCode (Effect)
79
+
80
+ Progressively reveals code lines with a typewriter-style animation.
81
+
82
+ ```js
83
+ new McCode.WriteCode(
84
+ { animatedAttrs: { writeCode: 1 } }, // 0 = hidden, 1 = fully visible
85
+ { selector: "!#code", duration: 3000 },
86
+ );
87
+ ```
88
+
89
+ ### CodeHighlight (Effect)
90
+
91
+ Pulses a line's background color with a double-pump effect. Each line needs a unique attr key `highlightLine_N` where N is the 0-based line index.
92
+
93
+ ```js
94
+ // Highlight line 5 (green)
95
+ new McCode.CodeHighlight(
96
+ { animatedAttrs: { highlightLine_5: 1 }, color: "rgba(46,160,67,0.3)" },
97
+ { selector: "!#code", duration: 800 },
98
+ );
99
+
100
+ // Multiple lines don't conflict because each has a unique key
101
+ new McCode.CodeHighlight(
102
+ { animatedAttrs: { highlightLine_8: 1 }, color: "rgba(231,111,81,0.3)" },
103
+ { selector: "!#code", duration: 800 },
104
+ );
105
+ ```
106
+
107
+ ## Line IDs
108
+
109
+ Each line in the rendered code gets an id: `line_0`, `line_1`, `line_2`, etc. (0-based).
110
+
111
+ ## Themes
112
+
113
+ ### Dark (default)
114
+ Dark background (#1e1e2e) with Catppuccin-inspired token colors.
115
+
116
+ ### Light
117
+ Light background (#fafafa) with One Light-inspired token colors.
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,365 @@
1
+ 'use strict';
2
+
3
+ var motorcortex = require('@donkeyclip/motorcortex');
4
+
5
+ /**
6
+ * CodeClip — a BrowserClip that renders syntax-highlighted code.
7
+ *
8
+ * Each line gets id: line_0, line_1, ... for targeting.
9
+ * Registers custom entity "code" for Effects via !#code.
10
+ *
11
+ * Attrs:
12
+ * code: string — the source code to display
13
+ * language: string (default: "javascript")
14
+ * theme: "dark" | "light" (default: "dark")
15
+ * fontSize: number (default: 14)
16
+ */
17
+
18
+ const THEMES = {
19
+ dark: {
20
+ bg: "#1e1e2e",
21
+ fg: "#cdd6f4",
22
+ lineNum: "#585b70",
23
+ keyword: "#cba6f7",
24
+ string: "#a6e3a1",
25
+ comment: "#6c7086",
26
+ fn: "#89b4fa",
27
+ number: "#fab387",
28
+ punct: "#94e2d5",
29
+ type: "#f9e2af"
30
+ },
31
+ light: {
32
+ bg: "#fafafa",
33
+ fg: "#383a42",
34
+ lineNum: "#9ca0b0",
35
+ keyword: "#a626a4",
36
+ string: "#50a14f",
37
+ comment: "#a0a1a7",
38
+ fn: "#4078f2",
39
+ number: "#986801",
40
+ punct: "#0184bc",
41
+ type: "#c18401"
42
+ }
43
+ };
44
+ const LANG_KEYWORDS = {
45
+ 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"]),
46
+ 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"]),
47
+ 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"]),
48
+ 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"]),
49
+ 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"]),
50
+ 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"])
51
+ };
52
+
53
+ // JSON/YAML don't have keywords in the traditional sense — handled by tokenizer rules
54
+ const COMMENT_STYLES = {
55
+ javascript: "//",
56
+ typescript: "//",
57
+ python: "#",
58
+ bash: "#",
59
+ yaml: "#",
60
+ css: null,
61
+ json: null,
62
+ html: null
63
+ };
64
+ function escapeHtml(s) {
65
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
66
+ }
67
+ function tokenizeLine(line, theme, language) {
68
+ const t = theme;
69
+ const keywords = LANG_KEYWORDS[language] || LANG_KEYWORDS.javascript;
70
+ const commentChar = COMMENT_STYLES[language] ?? "//";
71
+ let result = "";
72
+ let i = 0;
73
+
74
+ // YAML/JSON: color keys specially
75
+ if (language === "yaml") {
76
+ // YAML line: "key: value" or "- item"
77
+ const yamlKeyMatch = line.match(/^(\s*)([\w.\-/]+)(\s*:\s*)(.*)/);
78
+ if (yamlKeyMatch) {
79
+ const [, indent, key, colon, value] = yamlKeyMatch;
80
+ result += escapeHtml(indent);
81
+ result += `<span style="color:${t.fn}">${escapeHtml(key)}</span>`;
82
+ result += `<span style="color:${t.punct}">${escapeHtml(colon)}</span>`;
83
+ // Value coloring
84
+ if (/^(true|false|null|~)$/i.test(value.trim())) {
85
+ result += `<span style="color:${t.keyword};font-weight:600">${escapeHtml(value)}</span>`;
86
+ } else if (/^\d+(\.\d+)?$/.test(value.trim())) {
87
+ result += `<span style="color:${t.number}">${escapeHtml(value)}</span>`;
88
+ } else if (/^["']/.test(value.trim())) {
89
+ result += `<span style="color:${t.string}">${escapeHtml(value)}</span>`;
90
+ } else {
91
+ result += `<span style="color:${t.fg}">${escapeHtml(value)}</span>`;
92
+ }
93
+ return result || " ";
94
+ }
95
+ const yamlDash = line.match(/^(\s*)(- )(.*)/);
96
+ if (yamlDash) {
97
+ result += escapeHtml(yamlDash[1]);
98
+ result += `<span style="color:${t.punct}">${escapeHtml(yamlDash[2])}</span>`;
99
+ result += `<span style="color:${t.fg}">${escapeHtml(yamlDash[3])}</span>`;
100
+ return result || " ";
101
+ }
102
+ }
103
+ if (language === "json") {
104
+ // JSON: color keys in quotes before colon
105
+ const jsonKeyMatch = line.match(/^(\s*)("(?:[^"\\]|\\.)*")(\s*:\s*)(.*)/);
106
+ if (jsonKeyMatch) {
107
+ const [, indent, key, colon, value] = jsonKeyMatch;
108
+ result += escapeHtml(indent);
109
+ result += `<span style="color:${t.fn}">${escapeHtml(key)}</span>`;
110
+ result += `<span style="color:${t.punct}">${escapeHtml(colon)}</span>`;
111
+ if (/^(true|false|null)/.test(value.trim())) {
112
+ result += `<span style="color:${t.keyword};font-weight:600">${escapeHtml(value)}</span>`;
113
+ } else if (/^\d/.test(value.trim())) {
114
+ result += `<span style="color:${t.number}">${escapeHtml(value)}</span>`;
115
+ } else if (/^"/.test(value.trim())) {
116
+ result += `<span style="color:${t.string}">${escapeHtml(value)}</span>`;
117
+ } else {
118
+ result += `<span style="color:${t.fg}">${escapeHtml(value)}</span>`;
119
+ }
120
+ return result || " ";
121
+ }
122
+ }
123
+ while (i < line.length) {
124
+ // Comments: // or # to end of line
125
+ if (commentChar && line.startsWith(commentChar, i) && (i === 0 || /\s/.test(line[i - 1]) || i === 0)) {
126
+ result += `<span style="color:${t.comment};font-style:italic">${escapeHtml(line.slice(i))}</span>`;
127
+ break;
128
+ }
129
+
130
+ // Strings: single or double quoted
131
+ if (line[i] === '"' || line[i] === "'" || line[i] === "`") {
132
+ const q = line[i];
133
+ let j = i + 1;
134
+ while (j < line.length && line[j] !== q) {
135
+ if (line[j] === "\\") j++;
136
+ j++;
137
+ }
138
+ j = Math.min(j + 1, line.length);
139
+ result += `<span style="color:${t.string}">${escapeHtml(line.slice(i, j))}</span>`;
140
+ i = j;
141
+ continue;
142
+ }
143
+
144
+ // Numbers
145
+ if (/\d/.test(line[i]) && (i === 0 || /[\s,;(=+\-*/<>!&|]/.test(line[i - 1]))) {
146
+ let j = i;
147
+ while (j < line.length && /[\d.]/.test(line[j])) j++;
148
+ result += `<span style="color:${t.number}">${escapeHtml(line.slice(i, j))}</span>`;
149
+ i = j;
150
+ continue;
151
+ }
152
+
153
+ // Words (keywords, identifiers)
154
+ if (/[a-zA-Z_$]/.test(line[i])) {
155
+ let j = i;
156
+ while (j < line.length && /[a-zA-Z0-9_$]/.test(line[j])) j++;
157
+ const word = line.slice(i, j);
158
+ if (keywords.has(word)) {
159
+ result += `<span style="color:${t.keyword};font-weight:600">${escapeHtml(word)}</span>`;
160
+ } else if (j < line.length && line[j] === "(") {
161
+ result += `<span style="color:${t.fn}">${escapeHtml(word)}</span>`;
162
+ } else {
163
+ result += `<span style="color:${t.fg}">${escapeHtml(word)}</span>`;
164
+ }
165
+ i = j;
166
+ continue;
167
+ }
168
+
169
+ // Punctuation / operators
170
+ if (/[{}()\[\];:.,=+\-*/<>!&|?%^~@]/.test(line[i])) {
171
+ result += `<span style="color:${t.punct}">${escapeHtml(line[i])}</span>`;
172
+ i++;
173
+ continue;
174
+ }
175
+
176
+ // Whitespace and anything else
177
+ result += escapeHtml(line[i]);
178
+ i++;
179
+ }
180
+ return result || " ";
181
+ }
182
+ function renderLines(code, theme, language) {
183
+ const lines = code.split("\n");
184
+ return lines.map((line, i) => `<div class="shiki-line" id="line_${i}"><span class="line-num">${i + 1}</span><span class="line-code">${tokenizeLine(line, theme, language)}</span></div>`).join("");
185
+ }
186
+ class CodeClip extends motorcortex.BrowserClip {
187
+ get html() {
188
+ const code = this.attrs.code || "";
189
+ const language = this.attrs.language || "javascript";
190
+ const t = THEMES[this.attrs.theme] || THEMES.dark;
191
+ const linesHtml = renderLines(code, t, language);
192
+ return `<div id="code-container"><pre><code>${linesHtml}</code></pre></div>`;
193
+ }
194
+ get css() {
195
+ const fs = this.attrs.fontSize || 14;
196
+ const t = THEMES[this.attrs.theme] || THEMES.dark;
197
+ return `
198
+ #code-container {
199
+ width: 100%;
200
+ height: 100%;
201
+ display: flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ padding: 24px;
205
+ box-sizing: border-box;
206
+ background: ${t.bg};
207
+ }
208
+
209
+ pre {
210
+ margin: 0;
211
+ padding: 20px 24px;
212
+ border-radius: 10px;
213
+ background: ${t.bg};
214
+ font-size: ${fs}px;
215
+ line-height: 1.7;
216
+ overflow-x: auto;
217
+ width: 100%;
218
+ max-height: 100%;
219
+ box-sizing: border-box;
220
+ font-family: "Fira Code", "Cascadia Code", "JetBrains Mono", "Consolas", monospace;
221
+ }
222
+
223
+ code {
224
+ color: ${t.fg};
225
+ }
226
+
227
+ .shiki-line {
228
+ display: flex;
229
+ padding: 1px 8px;
230
+ border-radius: 4px;
231
+ gap: 16px;
232
+ }
233
+
234
+ .line-num {
235
+ color: ${t.lineNum};
236
+ user-select: none;
237
+ min-width: 2em;
238
+ text-align: right;
239
+ flex-shrink: 0;
240
+ }
241
+
242
+ .line-code {
243
+ white-space: pre;
244
+ }
245
+ `;
246
+ }
247
+ onAfterRender() {
248
+ this.attrs.code || "";
249
+ const container = this.context.getElements("#code-container")[0];
250
+ const lines = container.querySelectorAll(".shiki-line");
251
+ this.setCustomEntity("code", {
252
+ container,
253
+ lines,
254
+ lineCount: lines.length
255
+ });
256
+ }
257
+ }
258
+
259
+ /**
260
+ * CodeHighlight — Effect that pulses a line's background color.
261
+ *
262
+ * Selector: !#code
263
+ * animatedAttrs: { highlightLine_N: 1 } — where N is the line index (0-based)
264
+ * The line index is parsed from the attr key.
265
+ *
266
+ * Attrs:
267
+ * color: string (default: "rgba(99,102,241,0.25)") — highlight background
268
+ */
269
+ class CodeHighlight extends motorcortex.Effect {
270
+ getScratchValue() {
271
+ return 0;
272
+ }
273
+ onGetContext() {
274
+ this._lines = this.element?.entity?.lines;
275
+ this._color = this.attrs.color || "rgba(99,102,241,0.25)";
276
+ // Parse line index from attributeKey: "highlightLine_4" → 4
277
+ const key = this.attributeKey || "";
278
+ this._lineIndex = parseInt(key.split("_").pop(), 10);
279
+ }
280
+ onProgress(millisecond) {
281
+ if (!this._lines || isNaN(this._lineIndex) || this._lineIndex < 0 || this._lineIndex >= this._lines.length) return;
282
+ const fraction = this.getFraction(millisecond);
283
+ const line = this._lines[this._lineIndex];
284
+
285
+ // Double pump: 0→0.25 up, 0.25→0.5 down, 0.5→0.75 up, 0.75→1 down
286
+ let intensity;
287
+ if (fraction < 0.25) {
288
+ intensity = fraction / 0.25;
289
+ } else if (fraction < 0.5) {
290
+ intensity = 1 - (fraction - 0.25) / 0.25;
291
+ } else if (fraction < 0.75) {
292
+ intensity = (fraction - 0.5) / 0.25;
293
+ } else {
294
+ intensity = 1 - (fraction - 0.75) / 0.25;
295
+ }
296
+ if (intensity > 0.01) {
297
+ const alpha = intensity * 0.35;
298
+ const baseRgb = this._color.match(/\d+/g);
299
+ if (baseRgb && baseRgb.length >= 3) {
300
+ line.style.backgroundColor = `rgba(${baseRgb[0]},${baseRgb[1]},${baseRgb[2]},${alpha})`;
301
+ }
302
+ } else {
303
+ line.style.backgroundColor = "";
304
+ }
305
+ }
306
+ }
307
+
308
+ /**
309
+ * WriteCode — Effect that progressively reveals code lines.
310
+ *
311
+ * Selector: !#code
312
+ * animatedAttrs: { writeCode: 1 } — fraction 0→1 reveals all lines
313
+ */
314
+ class WriteCode extends motorcortex.Effect {
315
+ getScratchValue() {
316
+ return 0;
317
+ }
318
+ onGetContext() {
319
+ this._lines = this.element?.entity?.lines;
320
+ if (this._lines) {
321
+ // Hide all lines initially
322
+ for (const line of this._lines) {
323
+ line.style.opacity = "0";
324
+ }
325
+ }
326
+ }
327
+ onProgress(millisecond) {
328
+ if (!this._lines) return;
329
+ const fraction = this.getFraction(millisecond);
330
+ const initial = this.initialValue ?? 0;
331
+ const target = this.targetValue;
332
+ const current = initial + (target - initial) * fraction;
333
+ const total = this._lines.length;
334
+ const revealUpTo = current * total;
335
+ for (let i = 0; i < total; i++) {
336
+ if (i < Math.floor(revealUpTo)) {
337
+ this._lines[i].style.opacity = "1";
338
+ } else if (i < revealUpTo) {
339
+ this._lines[i].style.opacity = String(revealUpTo - i);
340
+ } else {
341
+ this._lines[i].style.opacity = "0";
342
+ }
343
+ }
344
+ }
345
+ }
346
+
347
+ var name = "@kissmybutton/mc-code";
348
+ var version = "0.0.1";
349
+
350
+ var index = {
351
+ npm_name: name,
352
+ version: version,
353
+ incidents: [{
354
+ exportable: CodeHighlight,
355
+ name: "CodeHighlight"
356
+ }, {
357
+ exportable: WriteCode,
358
+ name: "WriteCode"
359
+ }],
360
+ Clip: {
361
+ exportable: CodeClip
362
+ }
363
+ };
364
+
365
+ module.exports = index;