@human-synthesis/norns-core 0.0.5 → 0.0.7
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 +24 -40
- package/package.json +8 -11
- package/src/index.js +1 -2
- package/src/preprocess.js +415 -164
- package/src/uno.js +0 -66
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 + Civet** in `.n` files. The `.c` extension is recognised as an alias for `.civet` — both compile through Civet. CoffeeScript is no longer supported.
|
|
6
|
+
|
|
7
|
+
## Stack
|
|
8
|
+
|
|
9
|
+
- [Svelte 5](https://svelte.dev) — components and runes
|
|
10
|
+
- [Pug](https://pugjs.org) — templates
|
|
11
|
+
- [Civet](https://civet.dev) — script (TypeScript-flavored, indented)
|
|
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 — it adds the SvelteKit config, the Vite plugin, and the runtime layer.
|
|
22
|
+
|
|
11
23
|
## Usage
|
|
12
24
|
|
|
13
25
|
`svelte.config.js`:
|
|
@@ -16,47 +28,19 @@ 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
|
|
36
|
+
## What it does
|
|
57
37
|
|
|
58
|
-
|
|
38
|
+
- `.n` files default `<script>` to `lang="civet"` and `<template>` to `lang="pug"` — write neither attribute and it just works.
|
|
39
|
+
- `<script lang="civet">` blocks are compiled to JavaScript via [@danielx/civet](https://civet.dev) before svelte-preprocess sees them.
|
|
40
|
+
- Top-level Pug-only content is auto-wrapped in `<template lang="pug">` so you don't need the wrapper boilerplate.
|
|
41
|
+
- Pug class shorthand is rewritten so Tailwind variants (`.hover:bg-X`) and fractional values (`.gap-2.5`) work without escaping.
|
|
42
|
+
- `+if` / `+elseif` / `+else` chains are rewritten to Svelte block syntax (`{#if}/{:else if}/{:else}/{/if}`).
|
|
59
43
|
|
|
60
|
-
## License
|
|
44
|
+
## License
|
|
61
45
|
|
|
62
|
-
MIT © Human Synthesis. Built on top of [Svelte](https://github.com/sveltejs/svelte) © Svelte Contributors, MIT licensed.
|
|
46
|
+
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.7",
|
|
4
|
+
"description": "Norns core — Svelte preprocessor for Civet, 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,22 +13,19 @@
|
|
|
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
|
-
"@
|
|
29
|
-
"acorn": "^8.14.0",
|
|
30
|
-
"coffeescript": "^2.7.0",
|
|
31
|
-
"magic-string": "^0.30.10",
|
|
28
|
+
"@danielx/civet": "^0.11.0",
|
|
32
29
|
"pug": "^3.0.3",
|
|
33
30
|
"svelte-preprocess": "^6.0.3"
|
|
34
31
|
},
|
package/src/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export { nornsPreprocess
|
|
2
|
-
export { nornsUno } from './uno.js';
|
|
1
|
+
export { nornsPreprocess } from './preprocess.js';
|
package/src/preprocess.js
CHANGED
|
@@ -1,193 +1,399 @@
|
|
|
1
1
|
import { sveltePreprocess } from 'svelte-preprocess';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
2
|
+
import { compile as compileCivet } from '@danielx/civet';
|
|
3
|
+
|
|
4
|
+
export { transformIfChains, transformSnippets, rewritePugClasses };
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const SCRIPT_TAG = /<script\b([^>]*)>/i;
|
|
8
|
+
const TEMPLATE_TAG = /<template\b([^>]*)>/i;
|
|
9
|
+
|
|
10
|
+
function hasLangAttr(attrs) {
|
|
11
|
+
return /\blang\s*=/.test(attrs || '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* For .norn files: inject lang="civet" / lang="pug" defaults on
|
|
16
|
+
* <script> and <template> blocks, and auto-wrap any top-level non-script /
|
|
17
|
+
* non-style content in <template lang="pug">.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* If a .n file ends with an opening <script> or <style> tag but no matching
|
|
21
|
+
* close before EOF, append the closing tag. Lets users skip the boilerplate
|
|
22
|
+
* when the block is the very last thing in the file.
|
|
23
|
+
*/
|
|
24
|
+
function autoCloseTrailingBlock(content) {
|
|
25
|
+
let out = content;
|
|
26
|
+
for (const tag of ['script', 'style']) {
|
|
27
|
+
const opens = (out.match(new RegExp(`<${tag}\\b[^>]*>`, 'gi')) ?? []).length;
|
|
28
|
+
const closes = (out.match(new RegExp(`</${tag}>`, 'gi')) ?? []).length;
|
|
29
|
+
if (opens > closes) {
|
|
30
|
+
out = out.replace(/\s*$/, `\n</${tag}>\n`);
|
|
31
|
+
}
|
|
23
32
|
}
|
|
24
|
-
return
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const IF_RE = /^(\s*)\+if\s*\((.+)\)\s*$/;
|
|
37
|
+
const ELSEIF_RE_TPL = (ind) => new RegExp(`^${escapeRegex(ind)}\\+elseif\\s*\\((.+)\\)\\s*$`);
|
|
38
|
+
const ELSE_RE_TPL = (ind) => new RegExp(`^${escapeRegex(ind)}\\+else\\s*$`);
|
|
39
|
+
|
|
40
|
+
// `+snippet('name')` or `+snippet('name', arg1, arg2)`. Lazy match with `$`
|
|
41
|
+
// anchor lets the args list contain parens (e.g. `+snippet('row', fn(a))`)
|
|
42
|
+
// because the engine extends the lazy capture only until the outer `)` lands
|
|
43
|
+
// at end-of-line.
|
|
44
|
+
const SNIPPET_RE = /^(\s*)\+snippet\s*\(\s*['"](\w+)['"](?:\s*,\s*([\s\S]+?))?\s*\)\s*$/;
|
|
45
|
+
|
|
46
|
+
function escapeRegex(s) {
|
|
47
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stripQuotes(s) {
|
|
51
|
+
s = s.trim();
|
|
52
|
+
if (s.length >= 2) {
|
|
53
|
+
const first = s[0];
|
|
54
|
+
const last = s[s.length - 1];
|
|
55
|
+
if ((first === "'" || first === '"') && first === last) return s.slice(1, -1);
|
|
56
|
+
}
|
|
57
|
+
return s;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function detectIndentDiff(lines, fromIdx, parentIndent) {
|
|
61
|
+
for (let j = fromIdx; j < lines.length; j++) {
|
|
62
|
+
const line = lines[j];
|
|
63
|
+
if (line.trim() === '') continue;
|
|
64
|
+
const m = line.match(/^(\s*)/);
|
|
65
|
+
if (m && m[1].length > parentIndent.length) {
|
|
66
|
+
return m[1].slice(parentIndent.length);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
return '\t';
|
|
25
71
|
}
|
|
26
72
|
|
|
27
73
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
74
|
+
* Find the position of the matching `)` for an `(` at `start`, respecting
|
|
75
|
+
* nested parens and quoted strings.
|
|
76
|
+
*/
|
|
77
|
+
function findMatchingParen(str, start) {
|
|
78
|
+
if (str[start] !== '(') return -1;
|
|
79
|
+
let depth = 1;
|
|
80
|
+
let inSingle = false;
|
|
81
|
+
let inDouble = false;
|
|
82
|
+
for (let i = start + 1; i < str.length; i++) {
|
|
83
|
+
const c = str[i];
|
|
84
|
+
if (inSingle) {
|
|
85
|
+
if (c === "'" && str[i - 1] !== '\\') inSingle = false;
|
|
86
|
+
} else if (inDouble) {
|
|
87
|
+
if (c === '"' && str[i - 1] !== '\\') inDouble = false;
|
|
88
|
+
} else {
|
|
89
|
+
if (c === "'") inSingle = true;
|
|
90
|
+
else if (c === '"') inDouble = true;
|
|
91
|
+
else if (c === '(') depth++;
|
|
92
|
+
else if (c === ')') {
|
|
93
|
+
depth--;
|
|
94
|
+
if (depth === 0) return i;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const PUG_CLASS_SPECIAL = /[:.\/]/;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Rewrite Pug element lines whose class shorthand contains characters Pug's
|
|
105
|
+
* lexer rejects (`:`, `/`) or that Pug already mis-parses (fractional `.\d+`
|
|
106
|
+
* continuations like `.gap-2.5`). Route those classes from shorthand into the
|
|
107
|
+
* `(class="...")` attribute. Pug then sees only safe class shorthand.
|
|
31
108
|
*
|
|
32
|
-
*
|
|
109
|
+
* Examples:
|
|
110
|
+
* `.text-blue.hover:bg-red(href="/")`
|
|
111
|
+
* → `.text-blue(class="hover:bg-red" href="/")`
|
|
112
|
+
* `.gap-2.5.flex`
|
|
113
|
+
* → `.flex(class="gap-2.5")`
|
|
114
|
+
* `.bg-white/40.text-4xl(class="static")`
|
|
115
|
+
* → `.text-4xl(class="bg-white/40 static")`
|
|
33
116
|
*
|
|
34
|
-
*
|
|
117
|
+
* Skips lines inside `<script>` / `<style>` blocks.
|
|
35
118
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
allowReturnOutsideFunction: true,
|
|
43
|
-
allowAwaitOutsideFunction: true
|
|
44
|
-
});
|
|
45
|
-
} catch {
|
|
46
|
-
return code;
|
|
119
|
+
function rewritePugClasses(content) {
|
|
120
|
+
const blockRanges = [];
|
|
121
|
+
const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
|
|
122
|
+
let m;
|
|
123
|
+
while ((m = blockRe.exec(content)) !== null) {
|
|
124
|
+
blockRanges.push([m.index, m.index + m[0].length]);
|
|
47
125
|
}
|
|
48
126
|
|
|
49
|
-
const
|
|
50
|
-
|
|
127
|
+
const lines = content.split('\n');
|
|
128
|
+
let offset = 0;
|
|
129
|
+
const out = lines.map((line) => {
|
|
130
|
+
const lineEnd = offset + line.length;
|
|
131
|
+
const inBlock = blockRanges.some(([s, e]) => offset < e && lineEnd > s);
|
|
132
|
+
const result = inBlock ? line : rewritePugLine(line);
|
|
133
|
+
offset = lineEnd + 1; // +1 for the newline
|
|
134
|
+
return result;
|
|
135
|
+
});
|
|
136
|
+
return out.join('\n');
|
|
137
|
+
}
|
|
51
138
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
139
|
+
function rewritePugLine(line) {
|
|
140
|
+
const trimmed = line.trimStart();
|
|
141
|
+
if (!trimmed) return line;
|
|
142
|
+
const first = trimmed[0];
|
|
143
|
+
if (first === '|' || first === '<') return line; // text emit / raw HTML
|
|
144
|
+
if (trimmed.startsWith('//')) return line; // pug comment
|
|
145
|
+
if (first === '+' || first === ':') return line; // mixin call / pug filter
|
|
56
146
|
|
|
57
|
-
|
|
58
|
-
|
|
147
|
+
let i = 0;
|
|
148
|
+
while (i < line.length && /\s/.test(line[i])) i++;
|
|
149
|
+
const indent = line.slice(0, i);
|
|
59
150
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Pattern: X = expr (where X was declared via var earlier in this scope)
|
|
68
|
-
if (
|
|
69
|
-
e.type === 'AssignmentExpression' &&
|
|
70
|
-
e.operator === '=' &&
|
|
71
|
-
e.left.type === 'Identifier' &&
|
|
72
|
-
isVarDeclared(varStmts, e.left.name) &&
|
|
73
|
-
!claimed.has(e.left.name)
|
|
74
|
-
) {
|
|
75
|
-
claimed.add(e.left.name);
|
|
76
|
-
fused.add(e.left.name);
|
|
77
|
-
const init = code.slice(e.right.start, e.right.end);
|
|
78
|
-
const keyword = isRuneCall(e.right) ? 'let' : 'let';
|
|
79
|
-
s.overwrite(stmt.start, stmt.end, `${keyword} ${e.left.name} = ${init};`);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
151
|
+
let tag = '';
|
|
152
|
+
if (i < line.length && /[a-zA-Z]/.test(line[i])) {
|
|
153
|
+
let j = i;
|
|
154
|
+
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
155
|
+
tag = line.slice(i, j);
|
|
156
|
+
i = j;
|
|
157
|
+
}
|
|
82
158
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
const lhs = code.slice(e.left.start, e.left.end);
|
|
97
|
-
const rhs = code.slice(e.right.start, e.right.end);
|
|
98
|
-
s.overwrite(stmt.start, stmt.end, `let ${lhs} = ${rhs};`);
|
|
99
|
-
}
|
|
159
|
+
const segs = [];
|
|
160
|
+
while (i < line.length && (line[i] === '.' || line[i] === '#')) {
|
|
161
|
+
const sep = line[i];
|
|
162
|
+
let j = i + 1;
|
|
163
|
+
if (sep === '#') {
|
|
164
|
+
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
165
|
+
} else {
|
|
166
|
+
// class — extend chars to allow `/`, `:`, plus fractional `.\d+` suffixes
|
|
167
|
+
while (j < line.length && /[\w/:-]/.test(line[j])) j++;
|
|
168
|
+
while (j < line.length && line[j] === '.' && /\d/.test(line[j + 1] || '')) {
|
|
169
|
+
j++;
|
|
170
|
+
while (j < line.length && /\d/.test(line[j])) j++;
|
|
100
171
|
}
|
|
101
172
|
}
|
|
173
|
+
if (j === i + 1) break; // empty token, abort
|
|
174
|
+
segs.push(line.slice(i, j));
|
|
175
|
+
i = j;
|
|
176
|
+
}
|
|
102
177
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
.map((d) =>
|
|
112
|
-
d.init ? `${d.id.name} = ${code.slice(d.init.start, d.init.end)}` : d.id.name
|
|
113
|
-
)
|
|
114
|
-
.join(', ');
|
|
115
|
-
s.overwrite(stmt.start, stmt.end, `var ${rebuilt};`);
|
|
116
|
-
}
|
|
178
|
+
if (segs.length === 0) return line;
|
|
179
|
+
|
|
180
|
+
let attrs = '';
|
|
181
|
+
if (line[i] === '(') {
|
|
182
|
+
const close = findMatchingParen(line, i);
|
|
183
|
+
if (close !== -1) {
|
|
184
|
+
attrs = line.slice(i, close + 1);
|
|
185
|
+
i = close + 1;
|
|
117
186
|
}
|
|
187
|
+
}
|
|
188
|
+
const rest = line.slice(i);
|
|
118
189
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
190
|
+
const safe = [];
|
|
191
|
+
const routed = [];
|
|
192
|
+
for (const seg of segs) {
|
|
193
|
+
if (seg[0] === '#') {
|
|
194
|
+
safe.push(seg);
|
|
195
|
+
} else {
|
|
196
|
+
const cls = seg.slice(1);
|
|
197
|
+
if (PUG_CLASS_SPECIAL.test(cls)) routed.push(cls);
|
|
198
|
+
else safe.push(seg);
|
|
122
199
|
}
|
|
123
200
|
}
|
|
124
201
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
202
|
+
if (routed.length === 0) return line;
|
|
203
|
+
|
|
204
|
+
const newAttrs = mergeClassIntoAttrs(attrs, routed);
|
|
205
|
+
return `${indent}${tag}${safe.join('')}${newAttrs}${rest}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function mergeClassIntoAttrs(attrsStr, classesToAdd) {
|
|
209
|
+
const classStr = classesToAdd.join(' ');
|
|
210
|
+
if (!attrsStr) return `(class="${classStr}")`;
|
|
211
|
+
const inner = attrsStr.slice(1, -1);
|
|
212
|
+
|
|
213
|
+
// Existing static `class="..."` → prepend our routed classes.
|
|
214
|
+
const staticRe = /((?:^|\s)class\s*=\s*)"([^"]*)"/;
|
|
215
|
+
if (staticRe.test(inner)) {
|
|
216
|
+
return `(${inner.replace(staticRe, (_, prefix, val) => `${prefix}"${classStr} ${val}"`)})`;
|
|
217
|
+
}
|
|
218
|
+
// Existing dynamic `class!="{expr}"` → prepend static text. Svelte parses
|
|
219
|
+
// the resulting `class="static {expr}"` as text + interpolation.
|
|
220
|
+
const dynamicRe = /((?:^|\s)class\s*!=\s*)"([^"]*)"/;
|
|
221
|
+
if (dynamicRe.test(inner)) {
|
|
222
|
+
return `(${inner.replace(dynamicRe, (_, prefix, val) => `${prefix}"${classStr} ${val}"`)})`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// No class= attribute exists — insert one.
|
|
226
|
+
return `(class="${classStr}" ${inner})`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Rewrite Pug `+if('expr') / +elseif('expr') / +else` chains to raw Svelte
|
|
231
|
+
* block syntax emitted via Pug `|` text. Bypasses svelte-preprocess's `+if`
|
|
232
|
+
* mixin (which doesn't support chaining).
|
|
233
|
+
*
|
|
234
|
+
* Input:
|
|
235
|
+
* +if('a')
|
|
236
|
+
* div one
|
|
237
|
+
* +elseif('b')
|
|
238
|
+
* div two
|
|
239
|
+
* +else
|
|
240
|
+
* div three
|
|
241
|
+
*
|
|
242
|
+
* Output:
|
|
243
|
+
* | {#if a}
|
|
244
|
+
* div one
|
|
245
|
+
* | {:else if b}
|
|
246
|
+
* div two
|
|
247
|
+
* | {:else}
|
|
248
|
+
* div three
|
|
249
|
+
* | {/if}
|
|
250
|
+
*/
|
|
251
|
+
function transformIfChains(content) {
|
|
252
|
+
const lines = content.split('\n');
|
|
253
|
+
const out = [];
|
|
254
|
+
let i = 0;
|
|
255
|
+
|
|
256
|
+
while (i < lines.length) {
|
|
257
|
+
const m = lines[i].match(IF_RE);
|
|
258
|
+
if (!m) {
|
|
259
|
+
out.push(lines[i]);
|
|
260
|
+
i++;
|
|
261
|
+
continue;
|
|
130
262
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
263
|
+
|
|
264
|
+
const chainIndent = m[1];
|
|
265
|
+
const ifExpr = stripQuotes(m[2]);
|
|
266
|
+
const indentDiff = detectIndentDiff(lines, i + 1, chainIndent);
|
|
267
|
+
const elseIfRe = ELSEIF_RE_TPL(chainIndent);
|
|
268
|
+
const elseRe = ELSE_RE_TPL(chainIndent);
|
|
269
|
+
|
|
270
|
+
out.push(`${chainIndent}| {#if ${ifExpr}}`);
|
|
271
|
+
i++;
|
|
272
|
+
|
|
273
|
+
while (i < lines.length) {
|
|
274
|
+
const cur = lines[i];
|
|
275
|
+
|
|
276
|
+
const eIfM = cur.match(elseIfRe);
|
|
277
|
+
if (eIfM) {
|
|
278
|
+
out.push(`${chainIndent}| {:else if ${stripQuotes(eIfM[1])}}`);
|
|
279
|
+
i++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const eM = cur.match(elseRe);
|
|
283
|
+
if (eM) {
|
|
284
|
+
out.push(`${chainIndent}| {:else}`);
|
|
285
|
+
i++;
|
|
286
|
+
continue;
|
|
135
287
|
}
|
|
136
|
-
}
|
|
137
|
-
for (const key of Object.keys(node)) {
|
|
138
|
-
if (key === 'parent' || key === 'loc' || key === 'range') continue;
|
|
139
|
-
recurseInto(node[key]);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
288
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
289
|
+
if (cur.trim() === '') {
|
|
290
|
+
out.push(cur);
|
|
291
|
+
i++;
|
|
292
|
+
continue;
|
|
149
293
|
}
|
|
150
|
-
}
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
294
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
names.push(prop.argument.name);
|
|
295
|
+
const lineIndent = cur.match(/^(\s*)/)[1];
|
|
296
|
+
if (lineIndent.length > chainIndent.length) {
|
|
297
|
+
// Body line — de-indent by one level so it sits at the chain's level.
|
|
298
|
+
out.push(cur.startsWith(indentDiff) ? cur.slice(indentDiff.length) : cur);
|
|
299
|
+
i++;
|
|
300
|
+
continue;
|
|
161
301
|
}
|
|
302
|
+
|
|
303
|
+
break;
|
|
162
304
|
}
|
|
163
|
-
|
|
305
|
+
|
|
306
|
+
out.push(`${chainIndent}| {/if}`);
|
|
164
307
|
}
|
|
165
308
|
|
|
166
|
-
return
|
|
309
|
+
return out.join('\n');
|
|
167
310
|
}
|
|
168
311
|
|
|
169
|
-
|
|
170
|
-
|
|
312
|
+
/**
|
|
313
|
+
* Rewrite Pug `+snippet('name', args…)` blocks to Svelte 5 `{#snippet name(args)}`
|
|
314
|
+
* via Pug `|` text emit. Recurses into the body so nested snippets work
|
|
315
|
+
* (`Tabs > +snippet('item', tab) > Card > +snippet('header')`).
|
|
316
|
+
*
|
|
317
|
+
* Input:
|
|
318
|
+
* +snippet('header')
|
|
319
|
+
* h2 Title
|
|
320
|
+
*
|
|
321
|
+
* +snippet('row', user, idx)
|
|
322
|
+
* .row Hello {user.name} {idx}
|
|
323
|
+
*
|
|
324
|
+
* Output:
|
|
325
|
+
* | {#snippet header()}
|
|
326
|
+
* h2 Title
|
|
327
|
+
* | {/snippet}
|
|
328
|
+
*
|
|
329
|
+
* | {#snippet row(user, idx)}
|
|
330
|
+
* .row Hello {user.name} {idx}
|
|
331
|
+
* | {/snippet}
|
|
332
|
+
*/
|
|
333
|
+
function transformSnippets(content) {
|
|
334
|
+
const lines = content.split('\n');
|
|
335
|
+
const out = [];
|
|
336
|
+
let i = 0;
|
|
171
337
|
|
|
172
|
-
|
|
173
|
-
|
|
338
|
+
while (i < lines.length) {
|
|
339
|
+
const m = lines[i].match(SNIPPET_RE);
|
|
340
|
+
if (!m) {
|
|
341
|
+
out.push(lines[i]);
|
|
342
|
+
i++;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const indent = m[1];
|
|
347
|
+
const name = m[2];
|
|
348
|
+
const args = m[3] ? m[3].trim() : '';
|
|
349
|
+
const indentDiff = detectIndentDiff(lines, i + 1, indent);
|
|
350
|
+
|
|
351
|
+
out.push(`${indent}| {#snippet ${name}(${args})}`);
|
|
352
|
+
i++;
|
|
353
|
+
|
|
354
|
+
// Collect body lines (more indented than the +snippet header) and
|
|
355
|
+
// process them recursively so nested +snippet blocks resolve.
|
|
356
|
+
/** @type {string[]} */
|
|
357
|
+
const body = [];
|
|
358
|
+
while (i < lines.length) {
|
|
359
|
+
const cur = lines[i];
|
|
360
|
+
|
|
361
|
+
if (cur.trim() === '') {
|
|
362
|
+
body.push(cur);
|
|
363
|
+
i++;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const lineIndent = cur.match(/^(\s*)/)[1];
|
|
368
|
+
if (lineIndent.length > indent.length) {
|
|
369
|
+
body.push(cur.startsWith(indentDiff) ? cur.slice(indentDiff.length) : cur);
|
|
370
|
+
i++;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (body.length > 0) out.push(transformSnippets(body.join('\n')));
|
|
378
|
+
out.push(`${indent}| {/snippet}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return out.join('\n');
|
|
174
382
|
}
|
|
175
383
|
|
|
176
|
-
/**
|
|
177
|
-
* For .norn files: inject lang="coffee" / lang="pug" defaults on
|
|
178
|
-
* <script> and <template> blocks, and auto-wrap any top-level non-script /
|
|
179
|
-
* non-style content in <template lang="pug">.
|
|
180
|
-
*/
|
|
181
384
|
function nornDefaultLangs() {
|
|
182
385
|
return {
|
|
183
386
|
name: 'norns-default-langs',
|
|
184
387
|
markup({ content, filename }) {
|
|
185
|
-
if (!filename || !filename.endsWith('.
|
|
388
|
+
if (!filename || !filename.endsWith('.n')) return null;
|
|
186
389
|
|
|
187
|
-
let out = content;
|
|
390
|
+
let out = autoCloseTrailingBlock(content);
|
|
391
|
+
out = transformIfChains(out);
|
|
392
|
+
out = transformSnippets(out);
|
|
393
|
+
out = rewritePugClasses(out);
|
|
188
394
|
|
|
189
395
|
// If no <template> exists, scan for script/style blocks and wrap the rest.
|
|
190
|
-
if (!TEMPLATE_TAG.test(
|
|
396
|
+
if (!TEMPLATE_TAG.test(out)) {
|
|
191
397
|
const blocks = [];
|
|
192
398
|
const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
|
|
193
399
|
let m;
|
|
@@ -211,9 +417,9 @@ function nornDefaultLangs() {
|
|
|
211
417
|
}
|
|
212
418
|
}
|
|
213
419
|
|
|
214
|
-
// Inject lang="
|
|
420
|
+
// Inject lang="civet" on <script> tags missing lang=
|
|
215
421
|
out = out.replace(SCRIPT_TAG, (full, attrs) =>
|
|
216
|
-
hasLangAttr(attrs) ? full : `<script lang="
|
|
422
|
+
hasLangAttr(attrs) ? full : `<script lang="civet"${attrs}>`
|
|
217
423
|
);
|
|
218
424
|
|
|
219
425
|
// Inject lang="pug" on <template> tags missing lang=
|
|
@@ -226,37 +432,82 @@ function nornDefaultLangs() {
|
|
|
226
432
|
};
|
|
227
433
|
}
|
|
228
434
|
|
|
229
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Compile `<script lang="civet">` blocks to JavaScript via Civet.
|
|
437
|
+
*
|
|
438
|
+
* Runs before svelte-preprocess so that downstream stages see plain JS.
|
|
439
|
+
* Civet emits source maps; we forward them so devtools can resolve back to
|
|
440
|
+
* the original `.civet` source.
|
|
441
|
+
*
|
|
442
|
+
* Civet's emit characteristics (verified May 2026, civet@0.11):
|
|
443
|
+
* - `count .= $state 0` → `let count = $state(0)`
|
|
444
|
+
* - `count := $state 0` → `const count = $state(0)`
|
|
445
|
+
* - `{ a, b = 0 } := $props()` → `const { a, b = 0 } = $props()`
|
|
446
|
+
* - imports stay where written; if user writes them at top, output is fine
|
|
447
|
+
*
|
|
448
|
+
* No `var X; X = expr` split, so `nornCoffeeRuneFusion` and
|
|
449
|
+
* `nornsCoffeeImportLift` aren't needed for Civet sources.
|
|
450
|
+
*/
|
|
451
|
+
function nornsCivetScript() {
|
|
230
452
|
return {
|
|
231
|
-
name: 'norns-
|
|
232
|
-
script({ content, attributes }) {
|
|
233
|
-
if (attributes.lang !== '
|
|
234
|
-
const
|
|
235
|
-
|
|
453
|
+
name: 'norns-civet-script',
|
|
454
|
+
async script({ content, attributes, filename }) {
|
|
455
|
+
if (attributes.lang !== 'civet' && attributes.lang !== 'cv') return null;
|
|
456
|
+
const result = await compileCivet(content, {
|
|
457
|
+
js: true,
|
|
458
|
+
sourceMap: true,
|
|
459
|
+
filename: filename ?? 'unknown'
|
|
460
|
+
});
|
|
461
|
+
// Drop the `lang` attribute so svelte-preprocess doesn't try to load
|
|
462
|
+
// a `./transformers/civet` module — at this point the script body is
|
|
463
|
+
// already plain JS, no further script-level transform needed.
|
|
464
|
+
const { lang: _drop, ...nextAttrs } = attributes;
|
|
465
|
+
return {
|
|
466
|
+
code: result.code,
|
|
467
|
+
map: result.sourceMap?.json?.(filename ?? 'unknown') ?? null,
|
|
468
|
+
attributes: nextAttrs
|
|
469
|
+
};
|
|
236
470
|
}
|
|
237
471
|
};
|
|
238
472
|
}
|
|
239
473
|
|
|
474
|
+
|
|
240
475
|
/**
|
|
241
476
|
* Norns preprocessor stack.
|
|
242
477
|
*
|
|
243
|
-
* - `.norn` files default `<script>` to
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* -
|
|
247
|
-
*
|
|
248
|
-
*
|
|
478
|
+
* - `.norn` files default `<script>` to Civet and `<template>` to Pug (and
|
|
479
|
+
* auto-wrap top-level content in `<template lang="pug">` if no template
|
|
480
|
+
* block is present).
|
|
481
|
+
* - `<script lang="civet">` blocks are compiled to JS via @danielx/civet
|
|
482
|
+
* before svelte-preprocess sees them. Civet emits ESM-correct
|
|
483
|
+
* `let count = $state(0)` directly, so no rune-fusion or import-lift
|
|
484
|
+
* passes are needed.
|
|
485
|
+
*
|
|
486
|
+
* CoffeeScript is no longer supported — `lang="coffee"` will fail. Use
|
|
487
|
+
* `lang="civet"` (or omit lang and rely on the default).
|
|
249
488
|
*
|
|
250
489
|
* @param {import('svelte-preprocess').AutoPreprocessOptions} [options]
|
|
251
490
|
*/
|
|
252
491
|
export function nornsPreprocess(options = {}) {
|
|
253
492
|
return [
|
|
254
493
|
nornDefaultLangs(),
|
|
494
|
+
nornsCivetScript(),
|
|
255
495
|
sveltePreprocess({
|
|
256
|
-
coffeescript: { bare: true },
|
|
257
496
|
pug: {},
|
|
497
|
+
typescript: {
|
|
498
|
+
compilerOptions: {
|
|
499
|
+
// Silence TS 6.x's deprecation warning for older moduleResolution
|
|
500
|
+
// values (node10) that some toolchains still default to.
|
|
501
|
+
ignoreDeprecations: '6.0',
|
|
502
|
+
// Preserve value imports (Svelte component imports look "unused"
|
|
503
|
+
// to the TS transpiler since their usage lives in the template,
|
|
504
|
+
// but they MUST be emitted). verbatimModuleSyntax keeps any
|
|
505
|
+
// non-`import type` imports verbatim.
|
|
506
|
+
verbatimModuleSyntax: true,
|
|
507
|
+
isolatedModules: true
|
|
508
|
+
}
|
|
509
|
+
},
|
|
258
510
|
...options
|
|
259
|
-
})
|
|
260
|
-
nornCoffeeRuneFusion()
|
|
511
|
+
})
|
|
261
512
|
];
|
|
262
513
|
}
|
package/src/uno.js
DELETED
|
@@ -1,66 +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
|
-
import extractorPug from '@unocss/extractor-pug';
|
|
11
|
-
|
|
12
|
-
const NORN_PIPELINE = [
|
|
13
|
-
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
|
|
14
|
-
/\.(norn|coffee)($|\?)/
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Norns UnoCSS Vite plugin.
|
|
19
|
-
*
|
|
20
|
-
* Defaults baked in for the Norns stack:
|
|
21
|
-
* - presets: Uno + Attributify + Icons + Typography
|
|
22
|
-
* - transformers: Directives (`@apply`) + VariantGroup
|
|
23
|
-
* - extractor: extractor-pug, so Pug class shorthand inside `.svelte` and
|
|
24
|
-
* `.norn` files is recognized
|
|
25
|
-
* - content pipeline includes `.norn` and `.coffee`
|
|
26
|
-
* - hmrTopLevelAwait: false to avoid a TDZ error in WebKit/Safari when
|
|
27
|
-
* `virtual:uno.css` is imported from a SvelteKit route module
|
|
28
|
-
*
|
|
29
|
-
* **Naming caveat for shortcuts:** avoid names starting with a CSS
|
|
30
|
-
* pseudo-class prefix (`hover-`, `focus-`, `link-`, `active-`, `visited-`,
|
|
31
|
-
* `disabled-`, `checked-`, etc.) — UnoCSS parses those as variant + utility,
|
|
32
|
-
* not as a shortcut name. Use `nav-link`, `lk-muted`, etc. instead of
|
|
33
|
-
* `link-muted`.
|
|
34
|
-
*
|
|
35
|
-
* @param {import('unocss/vite').VitePluginConfig} [options]
|
|
36
|
-
*/
|
|
37
|
-
export function nornsUno(options = {}) {
|
|
38
|
-
const {
|
|
39
|
-
presets = [],
|
|
40
|
-
transformers = [],
|
|
41
|
-
extractors = [],
|
|
42
|
-
content = {},
|
|
43
|
-
hmrTopLevelAwait = false,
|
|
44
|
-
...rest
|
|
45
|
-
} = options;
|
|
46
|
-
return UnoCSS({
|
|
47
|
-
presets: [
|
|
48
|
-
presetUno(),
|
|
49
|
-
presetAttributify(),
|
|
50
|
-
presetIcons(),
|
|
51
|
-
presetTypography(),
|
|
52
|
-
...presets
|
|
53
|
-
],
|
|
54
|
-
transformers: [transformerDirectives(), transformerVariantGroup(), ...transformers],
|
|
55
|
-
extractors: [extractorPug(), ...extractors],
|
|
56
|
-
content: {
|
|
57
|
-
pipeline: {
|
|
58
|
-
include: NORN_PIPELINE,
|
|
59
|
-
...(content.pipeline ?? {})
|
|
60
|
-
},
|
|
61
|
-
...content
|
|
62
|
-
},
|
|
63
|
-
hmrTopLevelAwait,
|
|
64
|
-
...rest
|
|
65
|
-
});
|
|
66
|
-
}
|