@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 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`) 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}`).
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.9",
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
- ".": "./src/index.js",
21
- "./preprocess": "./src/preprocess.js",
22
- "./package.json": "./package.json"
23
+ ".": {
24
+ "types": "./types/index.d.ts",
25
+ "default": "./src/index.js"
23
26
  },
24
- "peerDependencies": {
25
- "svelte": "^5.0.0"
26
- },
27
- "dependencies": {
28
- "@danielx/civet": "^0.11.0",
29
- "pug": "^3.0.3",
30
- "svelte-preprocess": "^6.0.3"
31
- },
32
- "engines": {
33
- "node": ">=18"
34
- },
35
- "publishConfig": {
36
- "access": "public"
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
- const result = await compileCivet(content, {
465
- js: true,
466
- sourceMap: true,
467
- filename: filename ?? 'unknown'
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
- sveltePreprocess({
501
- pug: {},
502
- typescript: {
503
- compilerOptions: {
504
- // Silence TS 6.x's deprecation warning for older moduleResolution
505
- // values (node10) that some toolchains still default to.
506
- ignoreDeprecations: '6.0',
507
- // Preserve value imports (Svelte component imports look "unused"
508
- // to the TS transpiler since their usage lives in the template,
509
- // but they MUST be emitted). verbatimModuleSyntax keeps any
510
- // non-`import type` imports verbatim.
511
- verbatimModuleSyntax: true,
512
- isolatedModules: true
513
- }
514
- },
515
- ...options
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
  }
@@ -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>;