@human-synthesis/norns-core 0.0.4 → 0.0.6
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 +18 -42
- package/package.json +7 -7
- package/src/index.js +0 -1
- package/src/preprocess.js +415 -31
- package/src/uno.js +0 -43
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Norns Core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**AI-driven software architecture and development framework, based on Svelte.**
|
|
4
|
+
|
|
5
|
+
Svelte preprocessor for the Norns stack: Pug + CoffeeScript in `.n` files, with the small set of fixes needed to make Svelte 5 runes feel native in Coffee.
|
|
6
|
+
|
|
7
|
+
## Stack
|
|
8
|
+
|
|
9
|
+
- [Svelte 5](https://svelte.dev) — components and runes
|
|
10
|
+
- [Pug](https://pugjs.org) — templates
|
|
11
|
+
- [CoffeeScript 2](https://coffeescript.org) — script
|
|
12
|
+
- [Vite](https://vitejs.dev) — bundler
|
|
13
|
+
- [bun](https://bun.sh) — runtime / package manager
|
|
4
14
|
|
|
5
15
|
## Install
|
|
6
16
|
|
|
7
17
|
```sh
|
|
8
|
-
|
|
18
|
+
bun add -D @human-synthesis/norns-core svelte
|
|
9
19
|
```
|
|
10
20
|
|
|
21
|
+
Most users want the umbrella package [`@human-synthesis/norns`](https://github.com/human-synthesis/norns) instead.
|
|
22
|
+
|
|
11
23
|
## Usage
|
|
12
24
|
|
|
13
25
|
`svelte.config.js`:
|
|
@@ -16,47 +28,11 @@ pnpm add -D @human-synthesis/norns-core svelte unocss vite
|
|
|
16
28
|
import { nornsPreprocess } from '@human-synthesis/norns-core/preprocess';
|
|
17
29
|
|
|
18
30
|
export default {
|
|
31
|
+
extensions: ['.svelte', '.n'],
|
|
19
32
|
preprocess: nornsPreprocess()
|
|
20
33
|
};
|
|
21
34
|
```
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```js
|
|
26
|
-
import { defineConfig } from 'vite';
|
|
27
|
-
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
28
|
-
import { nornsUno } from '@human-synthesis/norns-core/uno';
|
|
29
|
-
|
|
30
|
-
export default defineConfig({
|
|
31
|
-
plugins: [nornsUno(), svelte()]
|
|
32
|
-
});
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Writing components
|
|
36
|
-
|
|
37
|
-
```svelte
|
|
38
|
-
<template lang="pug">
|
|
39
|
-
h1.text-3xl.font-bold Hello {name}
|
|
40
|
-
button(on:click="{increment}") count is {count}
|
|
41
|
-
</template>
|
|
42
|
-
|
|
43
|
-
<script lang="coffee">
|
|
44
|
-
export let name = 'world'
|
|
45
|
-
count = 0
|
|
46
|
-
increment = -> count++
|
|
47
|
-
</script>
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Why this exists
|
|
51
|
-
|
|
52
|
-
Norns wraps Svelte with the language and styling defaults we like:
|
|
53
|
-
|
|
54
|
-
- **CoffeeScript** in `<script lang="coffee">` blocks
|
|
55
|
-
- **Pug** in `<template lang="pug">` blocks
|
|
56
|
-
- **UnoCSS** with `presetUno`, `presetAttributify`, `presetIcons`, `presetTypography` enabled
|
|
57
|
-
|
|
58
|
-
The wrapper is intentionally thin — under the hood it's just `svelte-preprocess` and `unocss/vite` with reasonable defaults. Override anything by passing options.
|
|
59
|
-
|
|
60
|
-
## License & attribution
|
|
36
|
+
## License
|
|
61
37
|
|
|
62
|
-
MIT © Human Synthesis. Built on top of [Svelte](https://github.com/sveltejs/svelte) © Svelte Contributors, MIT licensed.
|
|
38
|
+
MIT © Daniel Teodoroiu / [Human Synthesis](https://humansynthesis.ai). Built on top of [Svelte](https://github.com/sveltejs/svelte) © Svelte Contributors, MIT licensed.
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@human-synthesis/norns-core",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Norns core — Svelte
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Norns core — Svelte preprocessor for CoffeeScript, Pug, and .n files",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "
|
|
6
|
+
"author": "Daniel Teodoroiu (https://humansynthesis.ai)",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"src",
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "bun test"
|
|
18
|
+
},
|
|
16
19
|
"exports": {
|
|
17
20
|
".": "./src/index.js",
|
|
18
21
|
"./preprocess": "./src/preprocess.js",
|
|
19
|
-
"./uno": "./src/uno.js",
|
|
20
22
|
"./package.json": "./package.json"
|
|
21
23
|
},
|
|
22
24
|
"peerDependencies": {
|
|
23
|
-
"svelte": "^5.0.0"
|
|
24
|
-
"unocss": "^0.65.0",
|
|
25
|
-
"vite": "^5.0.0 || ^6.0.0"
|
|
25
|
+
"svelte": "^5.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"acorn": "^8.14.0",
|
package/src/index.js
CHANGED
package/src/preprocess.js
CHANGED
|
@@ -33,6 +33,8 @@ function isRuneCall(node) {
|
|
|
33
33
|
*
|
|
34
34
|
* Walks function bodies recursively so closures get the same treatment.
|
|
35
35
|
*/
|
|
36
|
+
export { transformIfChains, rewritePugClasses };
|
|
37
|
+
|
|
36
38
|
export function fuseRuneDeclarations(code) {
|
|
37
39
|
let ast;
|
|
38
40
|
try {
|
|
@@ -64,7 +66,17 @@ export function fuseRuneDeclarations(code) {
|
|
|
64
66
|
if (stmt.type !== 'ExpressionStatement') continue;
|
|
65
67
|
const e = stmt.expression;
|
|
66
68
|
|
|
67
|
-
// Pattern: X = expr (where X
|
|
69
|
+
// Pattern: X = expr (where X is var-declared earlier in this scope).
|
|
70
|
+
// We fuse to `let X = expr` for any expression. Coffee always emits
|
|
71
|
+
// `var X; X = ...` for top-level assignments, and Svelte 5 reports
|
|
72
|
+
// `non_reactive_update` warnings for any `var` that's reassigned —
|
|
73
|
+
// even if the user only wrote a single function declaration. We
|
|
74
|
+
// limit to TOP LEVEL only (no recursion into nested function bodies)
|
|
75
|
+
// to avoid MagicString chunk conflicts.
|
|
76
|
+
//
|
|
77
|
+
// Special-case: `$derived(IIFE())` — Coffee compiles `$derived do ->`
|
|
78
|
+
// to `$derived((function(){...})())`, which evaluates ONCE at
|
|
79
|
+
// definition time instead of reactively. Rewrite to `$derived.by(fn)`.
|
|
68
80
|
if (
|
|
69
81
|
e.type === 'AssignmentExpression' &&
|
|
70
82
|
e.operator === '=' &&
|
|
@@ -74,9 +86,17 @@ export function fuseRuneDeclarations(code) {
|
|
|
74
86
|
) {
|
|
75
87
|
claimed.add(e.left.name);
|
|
76
88
|
fused.add(e.left.name);
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
89
|
+
|
|
90
|
+
const rewrite = rewriteDerivedIIFE(e.right, code);
|
|
91
|
+
if (rewrite) {
|
|
92
|
+
s.overwrite(
|
|
93
|
+
stmt.start,
|
|
94
|
+
stmt.end,
|
|
95
|
+
`let ${e.left.name} = $derived.by(${rewrite});`
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
s.appendLeft(stmt.start, 'let ');
|
|
99
|
+
}
|
|
80
100
|
continue;
|
|
81
101
|
}
|
|
82
102
|
|
|
@@ -116,28 +136,13 @@ export function fuseRuneDeclarations(code) {
|
|
|
116
136
|
}
|
|
117
137
|
}
|
|
118
138
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!node || typeof node !== 'object') return;
|
|
127
|
-
if (Array.isArray(node)) {
|
|
128
|
-
node.forEach(recurseInto);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
132
|
-
if (node.body && node.body.type === 'BlockStatement') {
|
|
133
|
-
walkBody(node.body.body);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
for (const key of Object.keys(node)) {
|
|
138
|
-
if (key === 'parent' || key === 'loc' || key === 'range') continue;
|
|
139
|
-
recurseInto(node[key]);
|
|
140
|
-
}
|
|
139
|
+
// Note: we deliberately do NOT recurse into nested function bodies.
|
|
140
|
+
// Svelte runes ($state, $derived, etc.) only apply at component
|
|
141
|
+
// top-level. Inner function bodies (event handlers, callbacks) have
|
|
142
|
+
// `var X; X = ...` patterns from CoffeeScript output too, but they
|
|
143
|
+
// don't trigger Svelte warnings and don't need fusion. Recursing
|
|
144
|
+
// also creates MagicString chunk conflicts when an outer assignment
|
|
145
|
+
// is being modified at the same time as something inside its body.
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
function isVarDeclared(varStmts, name) {
|
|
@@ -151,13 +156,35 @@ export function fuseRuneDeclarations(code) {
|
|
|
151
156
|
return false;
|
|
152
157
|
}
|
|
153
158
|
|
|
159
|
+
/**
|
|
160
|
+
* If `node` is `$derived((function(){...})())` (the result of Coffee's
|
|
161
|
+
* `$derived do ->`), return the source text of the inner FunctionExpression
|
|
162
|
+
* so the caller can rewrite to `$derived.by(<fn>)`. Otherwise null.
|
|
163
|
+
*/
|
|
164
|
+
function rewriteDerivedIIFE(node, src) {
|
|
165
|
+
if (!node || node.type !== 'CallExpression') return null;
|
|
166
|
+
if (node.callee.type !== 'Identifier' || node.callee.name !== '$derived') return null;
|
|
167
|
+
if (node.arguments.length !== 1) return null;
|
|
168
|
+
const arg = node.arguments[0];
|
|
169
|
+
if (arg.type !== 'CallExpression') return null;
|
|
170
|
+
if (arg.arguments.length !== 0) return null;
|
|
171
|
+
const fn = arg.callee;
|
|
172
|
+
if (fn.type !== 'FunctionExpression' && fn.type !== 'ArrowFunctionExpression') return null;
|
|
173
|
+
return src.slice(fn.start, fn.end);
|
|
174
|
+
}
|
|
175
|
+
|
|
154
176
|
function collectPatternNames(pat) {
|
|
155
177
|
const names = [];
|
|
156
178
|
if (pat.type === 'ObjectPattern') {
|
|
157
179
|
for (const prop of pat.properties) {
|
|
158
|
-
if (prop.type === 'Property'
|
|
159
|
-
|
|
180
|
+
if (prop.type === 'Property') {
|
|
181
|
+
let v = prop.value;
|
|
182
|
+
// Unwrap default value: `{ x = 1 }` has Property → AssignmentPattern → Identifier
|
|
183
|
+
if (v.type === 'AssignmentPattern') v = v.left;
|
|
184
|
+
if (v.type === 'Identifier') names.push(v.name);
|
|
185
|
+
} else if (prop.type === 'RestElement' && prop.argument.type === 'Identifier') {
|
|
160
186
|
names.push(prop.argument.name);
|
|
187
|
+
}
|
|
161
188
|
}
|
|
162
189
|
}
|
|
163
190
|
return names;
|
|
@@ -178,16 +205,305 @@ function hasLangAttr(attrs) {
|
|
|
178
205
|
* <script> and <template> blocks, and auto-wrap any top-level non-script /
|
|
179
206
|
* non-style content in <template lang="pug">.
|
|
180
207
|
*/
|
|
208
|
+
/**
|
|
209
|
+
* If a .n file ends with an opening <script> or <style> tag but no matching
|
|
210
|
+
* close before EOF, append the closing tag. Lets users skip the boilerplate
|
|
211
|
+
* when the block is the very last thing in the file.
|
|
212
|
+
*/
|
|
213
|
+
function autoCloseTrailingBlock(content) {
|
|
214
|
+
let out = content;
|
|
215
|
+
for (const tag of ['script', 'style']) {
|
|
216
|
+
const opens = (out.match(new RegExp(`<${tag}\\b[^>]*>`, 'gi')) ?? []).length;
|
|
217
|
+
const closes = (out.match(new RegExp(`</${tag}>`, 'gi')) ?? []).length;
|
|
218
|
+
if (opens > closes) {
|
|
219
|
+
out = out.replace(/\s*$/, `\n</${tag}>\n`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const IF_RE = /^(\s*)\+if\s*\((.+)\)\s*$/;
|
|
226
|
+
const ELSEIF_RE_TPL = (ind) => new RegExp(`^${escapeRegex(ind)}\\+elseif\\s*\\((.+)\\)\\s*$`);
|
|
227
|
+
const ELSE_RE_TPL = (ind) => new RegExp(`^${escapeRegex(ind)}\\+else\\s*$`);
|
|
228
|
+
|
|
229
|
+
function escapeRegex(s) {
|
|
230
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function stripQuotes(s) {
|
|
234
|
+
s = s.trim();
|
|
235
|
+
if (s.length >= 2) {
|
|
236
|
+
const first = s[0];
|
|
237
|
+
const last = s[s.length - 1];
|
|
238
|
+
if ((first === "'" || first === '"') && first === last) return s.slice(1, -1);
|
|
239
|
+
}
|
|
240
|
+
return s;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function detectIndentDiff(lines, fromIdx, parentIndent) {
|
|
244
|
+
for (let j = fromIdx; j < lines.length; j++) {
|
|
245
|
+
const line = lines[j];
|
|
246
|
+
if (line.trim() === '') continue;
|
|
247
|
+
const m = line.match(/^(\s*)/);
|
|
248
|
+
if (m && m[1].length > parentIndent.length) {
|
|
249
|
+
return m[1].slice(parentIndent.length);
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
return '\t';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Find the position of the matching `)` for an `(` at `start`, respecting
|
|
258
|
+
* nested parens and quoted strings.
|
|
259
|
+
*/
|
|
260
|
+
function findMatchingParen(str, start) {
|
|
261
|
+
if (str[start] !== '(') return -1;
|
|
262
|
+
let depth = 1;
|
|
263
|
+
let inSingle = false;
|
|
264
|
+
let inDouble = false;
|
|
265
|
+
for (let i = start + 1; i < str.length; i++) {
|
|
266
|
+
const c = str[i];
|
|
267
|
+
if (inSingle) {
|
|
268
|
+
if (c === "'" && str[i - 1] !== '\\') inSingle = false;
|
|
269
|
+
} else if (inDouble) {
|
|
270
|
+
if (c === '"' && str[i - 1] !== '\\') inDouble = false;
|
|
271
|
+
} else {
|
|
272
|
+
if (c === "'") inSingle = true;
|
|
273
|
+
else if (c === '"') inDouble = true;
|
|
274
|
+
else if (c === '(') depth++;
|
|
275
|
+
else if (c === ')') {
|
|
276
|
+
depth--;
|
|
277
|
+
if (depth === 0) return i;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return -1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const PUG_CLASS_SPECIAL = /[:.\/]/;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Rewrite Pug element lines whose class shorthand contains characters Pug's
|
|
288
|
+
* lexer rejects (`:`, `/`) or that Pug already mis-parses (fractional `.\d+`
|
|
289
|
+
* continuations like `.gap-2.5`). Route those classes from shorthand into the
|
|
290
|
+
* `(class="...")` attribute. Pug then sees only safe class shorthand.
|
|
291
|
+
*
|
|
292
|
+
* Examples:
|
|
293
|
+
* `.text-blue.hover:bg-red(href="/")`
|
|
294
|
+
* → `.text-blue(class="hover:bg-red" href="/")`
|
|
295
|
+
* `.gap-2.5.flex`
|
|
296
|
+
* → `.flex(class="gap-2.5")`
|
|
297
|
+
* `.bg-white/40.text-4xl(class="static")`
|
|
298
|
+
* → `.text-4xl(class="bg-white/40 static")`
|
|
299
|
+
*
|
|
300
|
+
* Skips lines inside `<script>` / `<style>` blocks.
|
|
301
|
+
*/
|
|
302
|
+
function rewritePugClasses(content) {
|
|
303
|
+
const blockRanges = [];
|
|
304
|
+
const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
|
|
305
|
+
let m;
|
|
306
|
+
while ((m = blockRe.exec(content)) !== null) {
|
|
307
|
+
blockRanges.push([m.index, m.index + m[0].length]);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const lines = content.split('\n');
|
|
311
|
+
let offset = 0;
|
|
312
|
+
const out = lines.map((line) => {
|
|
313
|
+
const lineEnd = offset + line.length;
|
|
314
|
+
const inBlock = blockRanges.some(([s, e]) => offset < e && lineEnd > s);
|
|
315
|
+
const result = inBlock ? line : rewritePugLine(line);
|
|
316
|
+
offset = lineEnd + 1; // +1 for the newline
|
|
317
|
+
return result;
|
|
318
|
+
});
|
|
319
|
+
return out.join('\n');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function rewritePugLine(line) {
|
|
323
|
+
const trimmed = line.trimStart();
|
|
324
|
+
if (!trimmed) return line;
|
|
325
|
+
const first = trimmed[0];
|
|
326
|
+
if (first === '|' || first === '<') return line; // text emit / raw HTML
|
|
327
|
+
if (trimmed.startsWith('//')) return line; // pug comment
|
|
328
|
+
if (first === '+' || first === ':') return line; // mixin call / pug filter
|
|
329
|
+
|
|
330
|
+
let i = 0;
|
|
331
|
+
while (i < line.length && /\s/.test(line[i])) i++;
|
|
332
|
+
const indent = line.slice(0, i);
|
|
333
|
+
|
|
334
|
+
let tag = '';
|
|
335
|
+
if (i < line.length && /[a-zA-Z]/.test(line[i])) {
|
|
336
|
+
let j = i;
|
|
337
|
+
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
338
|
+
tag = line.slice(i, j);
|
|
339
|
+
i = j;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const segs = [];
|
|
343
|
+
while (i < line.length && (line[i] === '.' || line[i] === '#')) {
|
|
344
|
+
const sep = line[i];
|
|
345
|
+
let j = i + 1;
|
|
346
|
+
if (sep === '#') {
|
|
347
|
+
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
348
|
+
} else {
|
|
349
|
+
// class — extend chars to allow `/`, `:`, plus fractional `.\d+` suffixes
|
|
350
|
+
while (j < line.length && /[\w/:-]/.test(line[j])) j++;
|
|
351
|
+
while (j < line.length && line[j] === '.' && /\d/.test(line[j + 1] || '')) {
|
|
352
|
+
j++;
|
|
353
|
+
while (j < line.length && /\d/.test(line[j])) j++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (j === i + 1) break; // empty token, abort
|
|
357
|
+
segs.push(line.slice(i, j));
|
|
358
|
+
i = j;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (segs.length === 0) return line;
|
|
362
|
+
|
|
363
|
+
let attrs = '';
|
|
364
|
+
if (line[i] === '(') {
|
|
365
|
+
const close = findMatchingParen(line, i);
|
|
366
|
+
if (close !== -1) {
|
|
367
|
+
attrs = line.slice(i, close + 1);
|
|
368
|
+
i = close + 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const rest = line.slice(i);
|
|
372
|
+
|
|
373
|
+
const safe = [];
|
|
374
|
+
const routed = [];
|
|
375
|
+
for (const seg of segs) {
|
|
376
|
+
if (seg[0] === '#') {
|
|
377
|
+
safe.push(seg);
|
|
378
|
+
} else {
|
|
379
|
+
const cls = seg.slice(1);
|
|
380
|
+
if (PUG_CLASS_SPECIAL.test(cls)) routed.push(cls);
|
|
381
|
+
else safe.push(seg);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (routed.length === 0) return line;
|
|
386
|
+
|
|
387
|
+
const newAttrs = mergeClassIntoAttrs(attrs, routed);
|
|
388
|
+
return `${indent}${tag}${safe.join('')}${newAttrs}${rest}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function mergeClassIntoAttrs(attrsStr, classesToAdd) {
|
|
392
|
+
const classStr = classesToAdd.join(' ');
|
|
393
|
+
if (!attrsStr) return `(class="${classStr}")`;
|
|
394
|
+
const inner = attrsStr.slice(1, -1);
|
|
395
|
+
|
|
396
|
+
// Existing static `class="..."` → prepend our routed classes.
|
|
397
|
+
const staticRe = /((?:^|\s)class\s*=\s*)"([^"]*)"/;
|
|
398
|
+
if (staticRe.test(inner)) {
|
|
399
|
+
return `(${inner.replace(staticRe, (_, prefix, val) => `${prefix}"${classStr} ${val}"`)})`;
|
|
400
|
+
}
|
|
401
|
+
// Existing dynamic `class!="{expr}"` → prepend static text. Svelte parses
|
|
402
|
+
// the resulting `class="static {expr}"` as text + interpolation.
|
|
403
|
+
const dynamicRe = /((?:^|\s)class\s*!=\s*)"([^"]*)"/;
|
|
404
|
+
if (dynamicRe.test(inner)) {
|
|
405
|
+
return `(${inner.replace(dynamicRe, (_, prefix, val) => `${prefix}"${classStr} ${val}"`)})`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// No class= attribute exists — insert one.
|
|
409
|
+
return `(class="${classStr}" ${inner})`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Rewrite Pug `+if('expr') / +elseif('expr') / +else` chains to raw Svelte
|
|
414
|
+
* block syntax emitted via Pug `|` text. Bypasses svelte-preprocess's `+if`
|
|
415
|
+
* mixin (which doesn't support chaining).
|
|
416
|
+
*
|
|
417
|
+
* Input:
|
|
418
|
+
* +if('a')
|
|
419
|
+
* div one
|
|
420
|
+
* +elseif('b')
|
|
421
|
+
* div two
|
|
422
|
+
* +else
|
|
423
|
+
* div three
|
|
424
|
+
*
|
|
425
|
+
* Output:
|
|
426
|
+
* | {#if a}
|
|
427
|
+
* div one
|
|
428
|
+
* | {:else if b}
|
|
429
|
+
* div two
|
|
430
|
+
* | {:else}
|
|
431
|
+
* div three
|
|
432
|
+
* | {/if}
|
|
433
|
+
*/
|
|
434
|
+
function transformIfChains(content) {
|
|
435
|
+
const lines = content.split('\n');
|
|
436
|
+
const out = [];
|
|
437
|
+
let i = 0;
|
|
438
|
+
|
|
439
|
+
while (i < lines.length) {
|
|
440
|
+
const m = lines[i].match(IF_RE);
|
|
441
|
+
if (!m) {
|
|
442
|
+
out.push(lines[i]);
|
|
443
|
+
i++;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const chainIndent = m[1];
|
|
448
|
+
const ifExpr = stripQuotes(m[2]);
|
|
449
|
+
const indentDiff = detectIndentDiff(lines, i + 1, chainIndent);
|
|
450
|
+
const elseIfRe = ELSEIF_RE_TPL(chainIndent);
|
|
451
|
+
const elseRe = ELSE_RE_TPL(chainIndent);
|
|
452
|
+
|
|
453
|
+
out.push(`${chainIndent}| {#if ${ifExpr}}`);
|
|
454
|
+
i++;
|
|
455
|
+
|
|
456
|
+
while (i < lines.length) {
|
|
457
|
+
const cur = lines[i];
|
|
458
|
+
|
|
459
|
+
const eIfM = cur.match(elseIfRe);
|
|
460
|
+
if (eIfM) {
|
|
461
|
+
out.push(`${chainIndent}| {:else if ${stripQuotes(eIfM[1])}}`);
|
|
462
|
+
i++;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const eM = cur.match(elseRe);
|
|
466
|
+
if (eM) {
|
|
467
|
+
out.push(`${chainIndent}| {:else}`);
|
|
468
|
+
i++;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (cur.trim() === '') {
|
|
473
|
+
out.push(cur);
|
|
474
|
+
i++;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const lineIndent = cur.match(/^(\s*)/)[1];
|
|
479
|
+
if (lineIndent.length > chainIndent.length) {
|
|
480
|
+
// Body line — de-indent by one level so it sits at the chain's level.
|
|
481
|
+
out.push(cur.startsWith(indentDiff) ? cur.slice(indentDiff.length) : cur);
|
|
482
|
+
i++;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
out.push(`${chainIndent}| {/if}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return out.join('\n');
|
|
493
|
+
}
|
|
494
|
+
|
|
181
495
|
function nornDefaultLangs() {
|
|
182
496
|
return {
|
|
183
497
|
name: 'norns-default-langs',
|
|
184
498
|
markup({ content, filename }) {
|
|
185
|
-
if (!filename || !filename.endsWith('.
|
|
499
|
+
if (!filename || !filename.endsWith('.n')) return null;
|
|
186
500
|
|
|
187
|
-
let out = content;
|
|
501
|
+
let out = autoCloseTrailingBlock(content);
|
|
502
|
+
out = transformIfChains(out);
|
|
503
|
+
out = rewritePugClasses(out);
|
|
188
504
|
|
|
189
505
|
// If no <template> exists, scan for script/style blocks and wrap the rest.
|
|
190
|
-
if (!TEMPLATE_TAG.test(
|
|
506
|
+
if (!TEMPLATE_TAG.test(out)) {
|
|
191
507
|
const blocks = [];
|
|
192
508
|
const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
|
|
193
509
|
let m;
|
|
@@ -237,6 +553,60 @@ function nornCoffeeRuneFusion() {
|
|
|
237
553
|
};
|
|
238
554
|
}
|
|
239
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Lift `import` statements to the top of a JS module.
|
|
558
|
+
*
|
|
559
|
+
* CoffeeScript 2 hoists all variables to a single `var x, y, z;` line at the
|
|
560
|
+
* very top of the file, then emits `import` statements *after* it. JS modules
|
|
561
|
+
* require imports above all other top-level statements, and svelte-preprocess
|
|
562
|
+
* + Svelte's MagicString-based pipeline don't handle this gracefully when
|
|
563
|
+
* there are also nested var/assignment patterns inside script function bodies.
|
|
564
|
+
*
|
|
565
|
+
* This pass parses the JS, finds all `ImportDeclaration` nodes, and rewrites
|
|
566
|
+
* them in source order at the very top of the module.
|
|
567
|
+
*/
|
|
568
|
+
export function liftImports(code) {
|
|
569
|
+
let ast;
|
|
570
|
+
try {
|
|
571
|
+
ast = parse(code, {
|
|
572
|
+
ecmaVersion: 'latest',
|
|
573
|
+
sourceType: 'module',
|
|
574
|
+
allowReturnOutsideFunction: true,
|
|
575
|
+
allowAwaitOutsideFunction: true
|
|
576
|
+
});
|
|
577
|
+
} catch {
|
|
578
|
+
return code;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const imports = ast.body.filter((s) => s.type === 'ImportDeclaration');
|
|
582
|
+
if (imports.length === 0) return code;
|
|
583
|
+
|
|
584
|
+
const firstNonImportIdx = ast.body.findIndex((s) => s.type !== 'ImportDeclaration');
|
|
585
|
+
if (firstNonImportIdx === -1) return code; // already all imports
|
|
586
|
+
|
|
587
|
+
const lateImports = imports.filter((i) => ast.body.indexOf(i) > firstNonImportIdx);
|
|
588
|
+
if (lateImports.length === 0) return code; // already in the right order
|
|
589
|
+
|
|
590
|
+
const s = new MagicString(code);
|
|
591
|
+
const importTexts = imports.map((i) => code.slice(i.start, i.end));
|
|
592
|
+
for (const i of imports) {
|
|
593
|
+
s.remove(i.start, i.end);
|
|
594
|
+
}
|
|
595
|
+
s.prependLeft(0, importTexts.join('\n') + '\n');
|
|
596
|
+
return s.toString();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function nornsCoffeeImportLift() {
|
|
600
|
+
return {
|
|
601
|
+
name: 'norns-coffee-import-lift',
|
|
602
|
+
script({ content, attributes }) {
|
|
603
|
+
if (attributes.lang !== 'coffee' && attributes.lang !== 'coffeescript') return null;
|
|
604
|
+
const code = liftImports(content);
|
|
605
|
+
return code === content ? null : { code };
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
240
610
|
/**
|
|
241
611
|
* Norns preprocessor stack.
|
|
242
612
|
*
|
|
@@ -255,8 +625,22 @@ export function nornsPreprocess(options = {}) {
|
|
|
255
625
|
sveltePreprocess({
|
|
256
626
|
coffeescript: { bare: true },
|
|
257
627
|
pug: {},
|
|
628
|
+
typescript: {
|
|
629
|
+
compilerOptions: {
|
|
630
|
+
// Silence TS 6.x's deprecation warning for older moduleResolution
|
|
631
|
+
// values (node10) that some toolchains still default to.
|
|
632
|
+
ignoreDeprecations: '6.0',
|
|
633
|
+
// Preserve value imports (Svelte component imports look "unused"
|
|
634
|
+
// to the TS transpiler since their usage lives in the template,
|
|
635
|
+
// but they MUST be emitted). verbatimModuleSyntax keeps any
|
|
636
|
+
// non-`import type` imports verbatim.
|
|
637
|
+
verbatimModuleSyntax: true,
|
|
638
|
+
isolatedModules: true
|
|
639
|
+
}
|
|
640
|
+
},
|
|
258
641
|
...options
|
|
259
642
|
}),
|
|
643
|
+
nornsCoffeeImportLift(),
|
|
260
644
|
nornCoffeeRuneFusion()
|
|
261
645
|
];
|
|
262
646
|
}
|
package/src/uno.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import UnoCSS from 'unocss/vite';
|
|
2
|
-
import {
|
|
3
|
-
presetUno,
|
|
4
|
-
presetAttributify,
|
|
5
|
-
presetIcons,
|
|
6
|
-
presetTypography,
|
|
7
|
-
transformerDirectives,
|
|
8
|
-
transformerVariantGroup
|
|
9
|
-
} from 'unocss';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Norns UnoCSS Vite plugin with a sensible preset stack.
|
|
13
|
-
*
|
|
14
|
-
* Includes transformerDirectives (so `@apply` works in `<style>` blocks)
|
|
15
|
-
* and transformerVariantGroup (so `class="hover:(text-blue underline)"`
|
|
16
|
-
* shorthand works).
|
|
17
|
-
*
|
|
18
|
-
* Defaults `hmrTopLevelAwait: false` to avoid a TDZ
|
|
19
|
-
* "Cannot access 'component' before initialization" error in WebKit/Safari
|
|
20
|
-
* when importing 'virtual:uno.css' from a SvelteKit layout/page module.
|
|
21
|
-
*
|
|
22
|
-
* @param {import('unocss/vite').VitePluginConfig} [options]
|
|
23
|
-
*/
|
|
24
|
-
export function nornsUno(options = {}) {
|
|
25
|
-
const {
|
|
26
|
-
presets = [],
|
|
27
|
-
transformers = [],
|
|
28
|
-
hmrTopLevelAwait = false,
|
|
29
|
-
...rest
|
|
30
|
-
} = options;
|
|
31
|
-
return UnoCSS({
|
|
32
|
-
presets: [
|
|
33
|
-
presetUno(),
|
|
34
|
-
presetAttributify(),
|
|
35
|
-
presetIcons(),
|
|
36
|
-
presetTypography(),
|
|
37
|
-
...presets
|
|
38
|
-
],
|
|
39
|
-
transformers: [transformerDirectives(), transformerVariantGroup(), ...transformers],
|
|
40
|
-
hmrTopLevelAwait,
|
|
41
|
-
...rest
|
|
42
|
-
});
|
|
43
|
-
}
|