@human-synthesis/norns-core 0.0.9 → 0.0.11
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/README.md +18 -3
- package/package.json +28 -19
- package/src/preprocess.js +388 -25
- package/types/index.d.ts +1 -0
- package/types/preprocess.d.ts +103 -0
package/README.md
CHANGED
|
@@ -37,9 +37,24 @@ export default {
|
|
|
37
37
|
|
|
38
38
|
- `.n` files default `<script>` to `lang="civet"` and `<template>` to `lang="pug"` — write neither attribute and it just works.
|
|
39
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`)
|
|
42
|
-
- `+if` / `+elseif` / `+else` chains are rewritten to Svelte block syntax (`{#if}/{:else if}/{:else}/{/if}`)
|
|
40
|
+
- Top-level Pug-only content is auto-wrapped in `<template lang="pug">` so you don't need the wrapper boilerplate; a trailing `<script>` block may omit its closing tag.
|
|
41
|
+
- Pug class shorthand is rewritten so Tailwind variants (`.hover:bg-X`), fractional values (`.gap-2.5`) and slashes (`.bg-white/40`) work without escaping.
|
|
42
|
+
- `+if` / `+elseif` / `+else` chains are rewritten to Svelte block syntax (`{#if}/{:else if}/{:else}/{/if}`), and `+snippet('name', args)` blocks to `{#snippet name(args)}`.
|
|
43
|
+
- Pug and Civet errors are reported as `file:line:column` **in the `.n` file you wrote**, with a code frame — not against svelte-preprocess's ~50-line mixin prelude or the script block's own numbering.
|
|
44
|
+
|
|
45
|
+
## Template syntax
|
|
46
|
+
|
|
47
|
+
| Write | Becomes |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `+if('cond')` … `+elseif('other')` … `+else` | `{#if cond}` … `{:else if other}` … `{:else}` … `{/if}` |
|
|
50
|
+
| `+each('items as item (item.id)')` | `{#each items as item (item.id)}` … `{/each}` — the Svelte `as` form; `item of items` is **not** valid |
|
|
51
|
+
| `+snippet('row', user, idx)` | `{#snippet row(user, idx)}` … `{/snippet}` |
|
|
52
|
+
| `\| {@render row(u, i)}` / `\| {@html raw}` | passed through as text — any line starting with `{` needs the `\| ` prefix |
|
|
53
|
+
| `attr!="{expr}"` | `attr={expr}` (Svelte expression); plain `attr="text"` stays a string |
|
|
54
|
+
| `.flex.items-center.gap-2.5(class="hover:bg-x")` | `class="flex items-center gap-2.5 hover:bg-x"` |
|
|
55
|
+
| `+key('expr')`, `+await('p')` / `+then('v')` / `+catch('e')` | the matching Svelte blocks (svelte-preprocess mixins) |
|
|
56
|
+
|
|
57
|
+
The `<script>` block is Civet by default (`lang="ts"` / `lang="js"` opt out): `{ a, b = 1 } := $props()`, `count .= $state 0` (use `.=` for anything you reassign), `total := $derived a + b`, `$effect => …`.
|
|
43
58
|
|
|
44
59
|
## Auto-imports — see the umbrella
|
|
45
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@human-synthesis/norns-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Norns core — Svelte preprocessor for Civet, Pug, and .n files",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Daniel Teodoroiu (https://humansynthesis.ai)",
|
|
@@ -11,28 +11,37 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"src",
|
|
14
|
+
"types",
|
|
14
15
|
"README.md"
|
|
15
16
|
],
|
|
17
|
+
"types": "./types/index.d.ts",
|
|
16
18
|
"scripts": {
|
|
17
|
-
"test": "bun test"
|
|
19
|
+
"test": "bun test",
|
|
20
|
+
"build:types": "tsc -p tsconfig.types.json"
|
|
18
21
|
},
|
|
19
22
|
"exports": {
|
|
20
|
-
".":
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./types/index.d.ts",
|
|
25
|
+
"default": "./src/index.js"
|
|
23
26
|
},
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
"./preprocess": {
|
|
28
|
+
"types": "./types/preprocess.d.ts",
|
|
29
|
+
"default": "./src/preprocess.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"svelte": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@danielx/civet": "^0.11.0",
|
|
38
|
+
"pug": "^3.0.3",
|
|
39
|
+
"svelte-preprocess": "^6.0.3"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
38
47
|
}
|
package/src/preprocess.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { sveltePreprocess } from 'svelte-preprocess';
|
|
2
2
|
import { compile as compileCivet } from '@danielx/civet';
|
|
3
3
|
|
|
4
|
-
export { transformIfChains, transformSnippets, rewritePugClasses };
|
|
5
|
-
|
|
4
|
+
export { transformIfChains, transformSnippets, rewritePugClasses, extractPugClasses };
|
|
6
5
|
|
|
7
6
|
const SCRIPT_TAG = /<script\b([^>]*)>/i;
|
|
8
7
|
const TEMPLATE_TAG = /<template\b([^>]*)>/i;
|
|
@@ -47,6 +46,259 @@ function escapeRegex(s) {
|
|
|
47
46
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
/* === Error mapping ===========================================================
|
|
50
|
+
*
|
|
51
|
+
* svelte-preprocess renders Pug with a ~50-line mixin prelude prepended, so
|
|
52
|
+
* raw Pug errors point ~50 lines past the real one; on top of that, `.n`
|
|
53
|
+
* files are re-arranged (template first, +if/+snippet rewritten) before Pug
|
|
54
|
+
* sees them. Civet errors inside `<script>` blocks are relative to the
|
|
55
|
+
* block, not the file. Both are mapped back to `file:line:column` in the
|
|
56
|
+
* source the user actually wrote, so an agent (or a human) opens the right
|
|
57
|
+
* line on the first try.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/** Original file contents, keyed by filename, stashed by nornDefaultLangs. */
|
|
61
|
+
const ORIGINAL_SOURCES = new Map();
|
|
62
|
+
const FALLBACK_PUG_PRELUDE = 52;
|
|
63
|
+
|
|
64
|
+
function rememberSource(filename, content) {
|
|
65
|
+
if (!filename) return;
|
|
66
|
+
if (ORIGINAL_SOURCES.size > 2000) ORIGINAL_SOURCES.clear();
|
|
67
|
+
ORIGINAL_SOURCES.set(filename, content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Parse Pug's numbered code frame out of an error message. */
|
|
71
|
+
function parsePugFrame(message) {
|
|
72
|
+
const frame = [];
|
|
73
|
+
let marked = null;
|
|
74
|
+
for (const line of String(message).split('\n')) {
|
|
75
|
+
const m = line.match(/^\s*(>)?\s*(\d+)\|(.*)$/);
|
|
76
|
+
if (!m) continue;
|
|
77
|
+
const n = Number(m[2]);
|
|
78
|
+
frame.push({ n, text: m[3].replace(/^ /, ''), marked: !!m[1] });
|
|
79
|
+
if (m[1]) marked = n;
|
|
80
|
+
}
|
|
81
|
+
return { frame, marked };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The human-readable core of a Pug error (last non-frame, non-location line). */
|
|
85
|
+
function pugMessageCore(message) {
|
|
86
|
+
const lines = String(message).split('\n');
|
|
87
|
+
let core = '';
|
|
88
|
+
for (const raw of lines) {
|
|
89
|
+
const line = raw.trim();
|
|
90
|
+
if (!line) continue;
|
|
91
|
+
if (/^\[svelte-preprocess\]/.test(line)) continue;
|
|
92
|
+
if (/^>?\s*\d+\|/.test(line)) continue;
|
|
93
|
+
if (/^-+\^?$/.test(line)) continue;
|
|
94
|
+
if (/^\S+:\d+:\d+$/.test(line)) continue;
|
|
95
|
+
core = line;
|
|
96
|
+
}
|
|
97
|
+
return core || 'Pug error';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractTemplate(content) {
|
|
101
|
+
const m = content.match(/<template\b[^>]*>([\s\S]*?)<\/template>/i);
|
|
102
|
+
return m ? m[1] : content;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reverse the `.n` rewrites for one template line so it can be looked up in
|
|
107
|
+
* the original source: `| {#if x}` came from `+if('x')`, etc.
|
|
108
|
+
*/
|
|
109
|
+
function sourceVariants(text) {
|
|
110
|
+
const t = text.trim();
|
|
111
|
+
const out = [text, t];
|
|
112
|
+
let m;
|
|
113
|
+
if ((m = t.match(/^\|\s*\{#if (.+)\}$/))) out.push(`+if('${m[1]}')`, `+if("${m[1]}")`);
|
|
114
|
+
else if ((m = t.match(/^\|\s*\{:else if (.+)\}$/)))
|
|
115
|
+
out.push(`+elseif('${m[1]}')`, `+elseif("${m[1]}")`);
|
|
116
|
+
else if (/^\|\s*\{:else\}$/.test(t)) out.push('+else');
|
|
117
|
+
else if ((m = t.match(/^\|\s*\{#snippet (\w+)\((.*)\)\}$/))) {
|
|
118
|
+
out.push(m[2] ? `+snippet('${m[1]}', ${m[2]})` : `+snippet('${m[1]}')`);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Find the original line for a (possibly rewritten) template line.
|
|
125
|
+
*
|
|
126
|
+
* @returns {{ line: number, exact: boolean }}
|
|
127
|
+
*/
|
|
128
|
+
function findOriginalLine(text, origLines, approx) {
|
|
129
|
+
const trimmed = text.trim();
|
|
130
|
+
const clamp = (n) => Math.min(Math.max(1, n), Math.max(1, origLines.length));
|
|
131
|
+
if (!trimmed) return { line: clamp(approx), exact: false };
|
|
132
|
+
|
|
133
|
+
const nearest = (idxs) =>
|
|
134
|
+
idxs.reduce(
|
|
135
|
+
(best, j) => (Math.abs(j + 1 - approx) < Math.abs(best + 1 - approx) ? j : best),
|
|
136
|
+
idxs[0]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
for (const variant of sourceVariants(text)) {
|
|
140
|
+
const v = variant.trim();
|
|
141
|
+
if (!v) continue;
|
|
142
|
+
const hits = [];
|
|
143
|
+
for (let j = 0; j < origLines.length; j++) {
|
|
144
|
+
if (origLines[j] === variant || origLines[j].trim() === v) hits.push(j);
|
|
145
|
+
}
|
|
146
|
+
if (hits.length === 1) return { line: hits[0] + 1, exact: true };
|
|
147
|
+
if (hits.length > 1) return { line: nearest(hits) + 1, exact: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Class-shorthand rewrites move classes into `(class="...")`; fall back to
|
|
151
|
+
// the tag/leading-class prefix.
|
|
152
|
+
const prefix = trimmed.match(/^[\w.#-]+/)?.[0];
|
|
153
|
+
if (prefix && prefix.length >= 3) {
|
|
154
|
+
const hits = [];
|
|
155
|
+
for (let j = 0; j < origLines.length; j++) {
|
|
156
|
+
if (origLines[j].trim().startsWith(prefix)) hits.push(j);
|
|
157
|
+
}
|
|
158
|
+
if (hits.length > 0) return { line: nearest(hits) + 1, exact: false };
|
|
159
|
+
}
|
|
160
|
+
return { line: clamp(approx), exact: false };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function frameOf(lines, line, column) {
|
|
164
|
+
const from = Math.max(1, line - 2);
|
|
165
|
+
const to = Math.min(lines.length, line + 1);
|
|
166
|
+
const width = String(to).length;
|
|
167
|
+
const out = [];
|
|
168
|
+
for (let n = from; n <= to; n++) {
|
|
169
|
+
out.push(`${n === line ? '>' : ' '} ${String(n).padStart(width)}| ${lines[n - 1] ?? ''}`);
|
|
170
|
+
if (n === line && column)
|
|
171
|
+
out.push(` ${' '.repeat(width)}| ${' '.repeat(Math.max(0, column - 1))}^`);
|
|
172
|
+
}
|
|
173
|
+
return out.join('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Turn a raw svelte-preprocess Pug error into one that points at the
|
|
178
|
+
* source file. Non-Pug errors pass through untouched.
|
|
179
|
+
*/
|
|
180
|
+
function mapPugError(e, content, filename) {
|
|
181
|
+
if (!e || typeof e.message !== 'string') return e;
|
|
182
|
+
if (!/Pug error|pug/i.test(e.message) && typeof e.line !== 'number') return e;
|
|
183
|
+
|
|
184
|
+
const { frame, marked } = parsePugFrame(e.message);
|
|
185
|
+
const pugLine = typeof e.line === 'number' ? e.line : marked;
|
|
186
|
+
if (!pugLine) return e;
|
|
187
|
+
|
|
188
|
+
const tplLines = extractTemplate(content).split('\n');
|
|
189
|
+
|
|
190
|
+
// Vote for the prelude offset using every frame line we can find verbatim
|
|
191
|
+
// in the template we handed to Pug.
|
|
192
|
+
const votes = new Map();
|
|
193
|
+
for (const f of frame) {
|
|
194
|
+
if (!f.text.trim()) continue;
|
|
195
|
+
for (let j = 0; j < tplLines.length; j++) {
|
|
196
|
+
if (tplLines[j] === f.text) {
|
|
197
|
+
const off = f.n - (j + 1);
|
|
198
|
+
votes.set(off, (votes.get(off) ?? 0) + 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
let offset = FALLBACK_PUG_PRELUDE;
|
|
203
|
+
let best = 0;
|
|
204
|
+
for (const [off, n] of votes) {
|
|
205
|
+
if (n > best) {
|
|
206
|
+
best = n;
|
|
207
|
+
offset = off;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tplLine = pugLine - offset;
|
|
212
|
+
const text = tplLines[tplLine - 1] ?? '';
|
|
213
|
+
|
|
214
|
+
const original = (filename && ORIGINAL_SOURCES.get(filename)) ?? content;
|
|
215
|
+
const origLines = original.split('\n');
|
|
216
|
+
const tplStart = Math.max(
|
|
217
|
+
0,
|
|
218
|
+
origLines.findIndex((l) => /<template\b/i.test(l))
|
|
219
|
+
);
|
|
220
|
+
const { line, exact } = findOriginalLine(text, origLines, tplLine + tplStart);
|
|
221
|
+
const column = typeof e.column === 'number' && e.column > 0 ? e.column : null;
|
|
222
|
+
|
|
223
|
+
const core = pugMessageCore(e.message);
|
|
224
|
+
const name = filename ? filename.split(/[\\/]/).pop() : 'template';
|
|
225
|
+
const frameText = frameOf(origLines, line, column);
|
|
226
|
+
const err = new Error(
|
|
227
|
+
`${name}:${line}:${column ?? 1}: Pug: ${core}${exact ? '' : ' (approximate line)'}\n\n${frameText}`
|
|
228
|
+
);
|
|
229
|
+
err.name = 'PugError';
|
|
230
|
+
err.code = 'norns_pug_error';
|
|
231
|
+
err.line = line;
|
|
232
|
+
err.column = column;
|
|
233
|
+
err.filename = filename;
|
|
234
|
+
err.frame = frameText;
|
|
235
|
+
err.approximate = !exact;
|
|
236
|
+
err.pugLine = pugLine;
|
|
237
|
+
err.cause = e;
|
|
238
|
+
return err;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Map a Civet ParseError thrown for a `<script>` block back to the line in
|
|
243
|
+
* the containing file.
|
|
244
|
+
*/
|
|
245
|
+
function mapCivetScriptError(e, content, filename) {
|
|
246
|
+
if (!e || typeof e.line !== 'number') return e;
|
|
247
|
+
const original = filename && ORIGINAL_SOURCES.get(filename);
|
|
248
|
+
if (!original) return e;
|
|
249
|
+
|
|
250
|
+
const blockRe = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
|
|
251
|
+
let m;
|
|
252
|
+
let bodyStart = -1;
|
|
253
|
+
let firstBodyStart = -1;
|
|
254
|
+
while ((m = blockRe.exec(original)) !== null) {
|
|
255
|
+
const start = m.index + m[0].indexOf('>') + 1;
|
|
256
|
+
if (firstBodyStart < 0) firstBodyStart = start;
|
|
257
|
+
if (m[1] === content) {
|
|
258
|
+
bodyStart = start;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (bodyStart < 0) bodyStart = firstBodyStart;
|
|
263
|
+
if (bodyStart < 0) return e;
|
|
264
|
+
|
|
265
|
+
const bodyStartLine = original.slice(0, bodyStart).split('\n').length;
|
|
266
|
+
const line = bodyStartLine + e.line - 1;
|
|
267
|
+
const column = typeof e.column === 'number' ? e.column : null;
|
|
268
|
+
const origLines = original.split('\n');
|
|
269
|
+
const name = filename.split(/[\\/]/).pop();
|
|
270
|
+
const rest = String(e.message).split('\n');
|
|
271
|
+
const head = rest.shift() ?? '';
|
|
272
|
+
const core = head.replace(/^\S+:\d+:\d+\s*/, '');
|
|
273
|
+
const frameText = frameOf(origLines, line, column);
|
|
274
|
+
e.message = [`${name}:${line}:${column ?? 1}: Civet: ${core}`, ...rest, '', frameText].join('\n');
|
|
275
|
+
e.line = line;
|
|
276
|
+
e.column = column;
|
|
277
|
+
e.filename = filename;
|
|
278
|
+
e.frame = frameText;
|
|
279
|
+
e.code = 'norns_civet_error';
|
|
280
|
+
return e;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Wrap svelte-preprocess so Pug failures come back mapped to the source.
|
|
285
|
+
*/
|
|
286
|
+
function withMappedPugErrors(sp) {
|
|
287
|
+
return {
|
|
288
|
+
...sp,
|
|
289
|
+
name: sp.name ?? 'norns-svelte-preprocess',
|
|
290
|
+
markup: sp.markup
|
|
291
|
+
? async (args) => {
|
|
292
|
+
try {
|
|
293
|
+
return await sp.markup(args);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
throw mapPugError(e, args.content, args.filename);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
: undefined
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
50
302
|
function stripQuotes(s) {
|
|
51
303
|
s = s.trim();
|
|
52
304
|
if (s.length >= 2) {
|
|
@@ -205,6 +457,110 @@ function rewritePugLine(line) {
|
|
|
205
457
|
return `${indent}${tag}${safe.join('')}${newAttrs}${rest}`;
|
|
206
458
|
}
|
|
207
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Walk `content` and return the set of Pug class-shorthand names found in
|
|
462
|
+
* element class chains (`.foo.bar-baz.hover:bg-red`) plus the value of any
|
|
463
|
+
* `class="..."` attribute on the same lines.
|
|
464
|
+
*
|
|
465
|
+
* Tailwind v4's content scanner extracts utility candidates from string
|
|
466
|
+
* contexts (`class="…"`, JS strings) but doesn't recognise Pug's chained
|
|
467
|
+
* shorthand — the dotted chain looks like one token. Pages render with the
|
|
468
|
+
* class names present in the markup but no matching CSS, which is silent
|
|
469
|
+
* and hard to spot. The companion `nornsTailwindPlugin()` Vite plugin in
|
|
470
|
+
* `@human-synthesis/norns` calls this and feeds the union into Tailwind via
|
|
471
|
+
* an injected `@source inline(...)` directive.
|
|
472
|
+
*
|
|
473
|
+
* Skips lines inside `<script>` / `<style>` blocks. Skips lines that begin
|
|
474
|
+
* with `|`, `<`, `+`, `:`, or `//` (Pug text emits, raw HTML, mixin calls,
|
|
475
|
+
* pug filters, and comments). For each remaining line, reads an optional
|
|
476
|
+
* tag, then chained `.<class>` segments (handling `:`, `/`, and fractional
|
|
477
|
+
* `.\d+` continuations), then collects the value of any `class="…"` or
|
|
478
|
+
* `class!="…"` attribute that follows.
|
|
479
|
+
*
|
|
480
|
+
* Pure function — does not mutate `content`. Returns a `Set<string>` so
|
|
481
|
+
* callers can union across many files without dedup work.
|
|
482
|
+
*
|
|
483
|
+
* @param {string} content
|
|
484
|
+
* @returns {Set<string>}
|
|
485
|
+
*/
|
|
486
|
+
function extractPugClasses(content) {
|
|
487
|
+
const out = new Set();
|
|
488
|
+
if (typeof content !== 'string' || content.length === 0) return out;
|
|
489
|
+
|
|
490
|
+
const blockRanges = [];
|
|
491
|
+
const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
|
|
492
|
+
let m;
|
|
493
|
+
while ((m = blockRe.exec(content)) !== null) {
|
|
494
|
+
blockRanges.push([m.index, m.index + m[0].length]);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const lines = content.split('\n');
|
|
498
|
+
let offset = 0;
|
|
499
|
+
for (const line of lines) {
|
|
500
|
+
const lineEnd = offset + line.length;
|
|
501
|
+
const inBlock = blockRanges.some(([s, e]) => offset < e && lineEnd > s);
|
|
502
|
+
if (!inBlock) collectFromPugLine(line, out);
|
|
503
|
+
offset = lineEnd + 1; // +1 for the newline
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function collectFromPugLine(line, into) {
|
|
509
|
+
const trimmed = line.trimStart();
|
|
510
|
+
if (!trimmed) return;
|
|
511
|
+
const first = trimmed[0];
|
|
512
|
+
if (first === '|' || first === '<') return;
|
|
513
|
+
if (trimmed.startsWith('//')) return;
|
|
514
|
+
if (first === '+' || first === ':') return;
|
|
515
|
+
|
|
516
|
+
let i = 0;
|
|
517
|
+
while (i < line.length && /\s/.test(line[i])) i++;
|
|
518
|
+
|
|
519
|
+
// Optional element tag.
|
|
520
|
+
if (i < line.length && /[a-zA-Z]/.test(line[i])) {
|
|
521
|
+
let j = i;
|
|
522
|
+
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
523
|
+
i = j;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// `.class` / `#id` segments. Mirrors `rewritePugLine` so the two stay
|
|
527
|
+
// in sync — both must accept the same chained-shorthand grammar.
|
|
528
|
+
while (i < line.length && (line[i] === '.' || line[i] === '#')) {
|
|
529
|
+
const sep = line[i];
|
|
530
|
+
let j = i + 1;
|
|
531
|
+
if (sep === '#') {
|
|
532
|
+
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
533
|
+
} else {
|
|
534
|
+
while (j < line.length && /[\w/:-]/.test(line[j])) j++;
|
|
535
|
+
while (j < line.length && line[j] === '.' && /\d/.test(line[j + 1] || '')) {
|
|
536
|
+
j++;
|
|
537
|
+
while (j < line.length && /\d/.test(line[j])) j++;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (j === i + 1) break;
|
|
541
|
+
if (sep === '.') into.add(line.slice(i + 1, j));
|
|
542
|
+
i = j;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// `(attrs)` block — pull class="..." and class!="..." values too. Pug
|
|
546
|
+
// chained shorthand often coexists with a `(class="...")` attribute on
|
|
547
|
+
// the same line (especially after `rewritePugClasses` routes special
|
|
548
|
+
// chars there). Capturing both lets a single pass cover the full set.
|
|
549
|
+
if (line[i] === '(') {
|
|
550
|
+
const close = findMatchingParen(line, i);
|
|
551
|
+
if (close !== -1) {
|
|
552
|
+
const attrs = line.slice(i + 1, close);
|
|
553
|
+
const re = /(?:^|\s)class\s*!?=\s*"([^"]*)"/g;
|
|
554
|
+
let am;
|
|
555
|
+
while ((am = re.exec(attrs)) !== null) {
|
|
556
|
+
for (const tok of am[1].split(/\s+/)) {
|
|
557
|
+
if (tok) into.add(tok);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
208
564
|
function mergeClassIntoAttrs(attrsStr, classesToAdd) {
|
|
209
565
|
const classStr = classesToAdd.join(' ');
|
|
210
566
|
if (!attrsStr) return `(class="${classStr}")`;
|
|
@@ -397,6 +753,7 @@ function nornDefaultLangs() {
|
|
|
397
753
|
name: 'norns-default-langs',
|
|
398
754
|
markup({ content, filename }) {
|
|
399
755
|
if (!filename || !filename.endsWith('.n')) return null;
|
|
756
|
+
rememberSource(filename, content);
|
|
400
757
|
|
|
401
758
|
let out = autoCloseTrailingBlock(content);
|
|
402
759
|
out = transformIfChains(out);
|
|
@@ -461,11 +818,16 @@ function nornsCivetScript() {
|
|
|
461
818
|
name: 'norns-civet-script',
|
|
462
819
|
async script({ content, attributes, filename }) {
|
|
463
820
|
if (attributes.lang !== 'civet' && attributes.lang !== 'cv') return null;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
821
|
+
let result;
|
|
822
|
+
try {
|
|
823
|
+
result = await compileCivet(content, {
|
|
824
|
+
js: true,
|
|
825
|
+
sourceMap: true,
|
|
826
|
+
filename: filename ?? 'unknown'
|
|
827
|
+
});
|
|
828
|
+
} catch (e) {
|
|
829
|
+
throw mapCivetScriptError(e, content, filename);
|
|
830
|
+
}
|
|
469
831
|
// Drop the `lang` attribute so svelte-preprocess doesn't try to load
|
|
470
832
|
// a `./transformers/civet` module — at this point the script body is
|
|
471
833
|
// already plain JS, no further script-level transform needed.
|
|
@@ -479,7 +841,6 @@ function nornsCivetScript() {
|
|
|
479
841
|
};
|
|
480
842
|
}
|
|
481
843
|
|
|
482
|
-
|
|
483
844
|
/**
|
|
484
845
|
* Norns preprocessor stack.
|
|
485
846
|
*
|
|
@@ -497,22 +858,24 @@ export function nornsPreprocess(options = {}) {
|
|
|
497
858
|
return [
|
|
498
859
|
nornDefaultLangs(),
|
|
499
860
|
nornsCivetScript(),
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
861
|
+
withMappedPugErrors(
|
|
862
|
+
sveltePreprocess({
|
|
863
|
+
pug: {},
|
|
864
|
+
typescript: {
|
|
865
|
+
compilerOptions: {
|
|
866
|
+
// Silence TS 6.x's deprecation warning for older moduleResolution
|
|
867
|
+
// values (node10) that some toolchains still default to.
|
|
868
|
+
ignoreDeprecations: '6.0',
|
|
869
|
+
// Preserve value imports (Svelte component imports look "unused"
|
|
870
|
+
// to the TS transpiler since their usage lives in the template,
|
|
871
|
+
// but they MUST be emitted). verbatimModuleSyntax keeps any
|
|
872
|
+
// non-`import type` imports verbatim.
|
|
873
|
+
verbatimModuleSyntax: true,
|
|
874
|
+
isolatedModules: true
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
...options
|
|
878
|
+
})
|
|
879
|
+
)
|
|
517
880
|
];
|
|
518
881
|
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { nornsPreprocess } from "./preprocess.js";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Norns preprocessor stack.
|
|
3
|
+
*
|
|
4
|
+
* - `.norn` files default `<script>` to Civet and `<template>` to Pug (and
|
|
5
|
+
* auto-wrap top-level content in `<template lang="pug">` if no template
|
|
6
|
+
* block is present).
|
|
7
|
+
* - `<script lang="civet">` blocks are compiled to JS via @danielx/civet
|
|
8
|
+
* before svelte-preprocess sees them. Civet emits ESM-correct
|
|
9
|
+
* `let count = $state(0)` directly, so no rune-fusion or import-lift
|
|
10
|
+
* passes are needed.
|
|
11
|
+
*
|
|
12
|
+
* @param {import('svelte-preprocess').AutoPreprocessOptions} [options]
|
|
13
|
+
*/
|
|
14
|
+
export function nornsPreprocess(options?: import("svelte-preprocess").AutoPreprocessOptions): any[];
|
|
15
|
+
/**
|
|
16
|
+
* Rewrite Pug `+if('expr') / +elseif('expr') / +else` chains to raw Svelte
|
|
17
|
+
* block syntax emitted via Pug `|` text. Bypasses svelte-preprocess's `+if`
|
|
18
|
+
* mixin (which doesn't support chaining).
|
|
19
|
+
*
|
|
20
|
+
* Input:
|
|
21
|
+
* +if('a')
|
|
22
|
+
* div one
|
|
23
|
+
* +elseif('b')
|
|
24
|
+
* div two
|
|
25
|
+
* +else
|
|
26
|
+
* div three
|
|
27
|
+
*
|
|
28
|
+
* Output:
|
|
29
|
+
* | {#if a}
|
|
30
|
+
* div one
|
|
31
|
+
* | {:else if b}
|
|
32
|
+
* div two
|
|
33
|
+
* | {:else}
|
|
34
|
+
* div three
|
|
35
|
+
* | {/if}
|
|
36
|
+
*/
|
|
37
|
+
export function transformIfChains(content: any): any;
|
|
38
|
+
/**
|
|
39
|
+
* Rewrite Pug `+snippet('name', args…)` blocks to Svelte 5 `{#snippet name(args)}`
|
|
40
|
+
* via Pug `|` text emit. Recurses into the body so nested snippets work
|
|
41
|
+
* (`Tabs > +snippet('item', tab) > Card > +snippet('header')`).
|
|
42
|
+
*
|
|
43
|
+
* Input:
|
|
44
|
+
* +snippet('header')
|
|
45
|
+
* h2 Title
|
|
46
|
+
*
|
|
47
|
+
* +snippet('row', user, idx)
|
|
48
|
+
* .row Hello {user.name} {idx}
|
|
49
|
+
*
|
|
50
|
+
* Output:
|
|
51
|
+
* | {#snippet header()}
|
|
52
|
+
* h2 Title
|
|
53
|
+
* | {/snippet}
|
|
54
|
+
*
|
|
55
|
+
* | {#snippet row(user, idx)}
|
|
56
|
+
* .row Hello {user.name} {idx}
|
|
57
|
+
* | {/snippet}
|
|
58
|
+
*/
|
|
59
|
+
export function transformSnippets(content: any): any;
|
|
60
|
+
/**
|
|
61
|
+
* Rewrite Pug element lines whose class shorthand contains characters Pug's
|
|
62
|
+
* lexer rejects (`:`, `/`) or that Pug already mis-parses (fractional `.\d+`
|
|
63
|
+
* continuations like `.gap-2.5`). Route those classes from shorthand into the
|
|
64
|
+
* `(class="...")` attribute. Pug then sees only safe class shorthand.
|
|
65
|
+
*
|
|
66
|
+
* Examples:
|
|
67
|
+
* `.text-blue.hover:bg-red(href="/")`
|
|
68
|
+
* → `.text-blue(class="hover:bg-red" href="/")`
|
|
69
|
+
* `.gap-2.5.flex`
|
|
70
|
+
* → `.flex(class="gap-2.5")`
|
|
71
|
+
* `.bg-white/40.text-4xl(class="static")`
|
|
72
|
+
* → `.text-4xl(class="bg-white/40 static")`
|
|
73
|
+
*
|
|
74
|
+
* Skips lines inside `<script>` / `<style>` blocks.
|
|
75
|
+
*/
|
|
76
|
+
export function rewritePugClasses(content: any): any;
|
|
77
|
+
/**
|
|
78
|
+
* Walk `content` and return the set of Pug class-shorthand names found in
|
|
79
|
+
* element class chains (`.foo.bar-baz.hover:bg-red`) plus the value of any
|
|
80
|
+
* `class="..."` attribute on the same lines.
|
|
81
|
+
*
|
|
82
|
+
* Tailwind v4's content scanner extracts utility candidates from string
|
|
83
|
+
* contexts (`class="…"`, JS strings) but doesn't recognise Pug's chained
|
|
84
|
+
* shorthand — the dotted chain looks like one token. Pages render with the
|
|
85
|
+
* class names present in the markup but no matching CSS, which is silent
|
|
86
|
+
* and hard to spot. The companion `nornsTailwindPlugin()` Vite plugin in
|
|
87
|
+
* `@human-synthesis/norns` calls this and feeds the union into Tailwind via
|
|
88
|
+
* an injected `@source inline(...)` directive.
|
|
89
|
+
*
|
|
90
|
+
* Skips lines inside `<script>` / `<style>` blocks. Skips lines that begin
|
|
91
|
+
* with `|`, `<`, `+`, `:`, or `//` (Pug text emits, raw HTML, mixin calls,
|
|
92
|
+
* pug filters, and comments). For each remaining line, reads an optional
|
|
93
|
+
* tag, then chained `.<class>` segments (handling `:`, `/`, and fractional
|
|
94
|
+
* `.\d+` continuations), then collects the value of any `class="…"` or
|
|
95
|
+
* `class!="…"` attribute that follows.
|
|
96
|
+
*
|
|
97
|
+
* Pure function — does not mutate `content`. Returns a `Set<string>` so
|
|
98
|
+
* callers can union across many files without dedup work.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} content
|
|
101
|
+
* @returns {Set<string>}
|
|
102
|
+
*/
|
|
103
|
+
export function extractPugClasses(content: string): Set<string>;
|