@human-synthesis/norns-core 0.0.8 → 0.0.10

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/preprocess.js +121 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@human-synthesis/norns-core",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
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)",
package/src/preprocess.js CHANGED
@@ -1,7 +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 };
4
+ export { transformIfChains, transformSnippets, rewritePugClasses, extractPugClasses };
5
5
 
6
6
 
7
7
  const SCRIPT_TAG = /<script\b([^>]*)>/i;
@@ -205,6 +205,110 @@ function rewritePugLine(line) {
205
205
  return `${indent}${tag}${safe.join('')}${newAttrs}${rest}`;
206
206
  }
207
207
 
208
+ /**
209
+ * Walk `content` and return the set of Pug class-shorthand names found in
210
+ * element class chains (`.foo.bar-baz.hover:bg-red`) plus the value of any
211
+ * `class="..."` attribute on the same lines.
212
+ *
213
+ * Tailwind v4's content scanner extracts utility candidates from string
214
+ * contexts (`class="…"`, JS strings) but doesn't recognise Pug's chained
215
+ * shorthand — the dotted chain looks like one token. Pages render with the
216
+ * class names present in the markup but no matching CSS, which is silent
217
+ * and hard to spot. The companion `nornsTailwindPlugin()` Vite plugin in
218
+ * `@human-synthesis/norns` calls this and feeds the union into Tailwind via
219
+ * an injected `@source inline(...)` directive.
220
+ *
221
+ * Skips lines inside `<script>` / `<style>` blocks. Skips lines that begin
222
+ * with `|`, `<`, `+`, `:`, or `//` (Pug text emits, raw HTML, mixin calls,
223
+ * pug filters, and comments). For each remaining line, reads an optional
224
+ * tag, then chained `.<class>` segments (handling `:`, `/`, and fractional
225
+ * `.\d+` continuations), then collects the value of any `class="…"` or
226
+ * `class!="…"` attribute that follows.
227
+ *
228
+ * Pure function — does not mutate `content`. Returns a `Set<string>` so
229
+ * callers can union across many files without dedup work.
230
+ *
231
+ * @param {string} content
232
+ * @returns {Set<string>}
233
+ */
234
+ function extractPugClasses(content) {
235
+ const out = new Set();
236
+ if (typeof content !== 'string' || content.length === 0) return out;
237
+
238
+ const blockRanges = [];
239
+ const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
240
+ let m;
241
+ while ((m = blockRe.exec(content)) !== null) {
242
+ blockRanges.push([m.index, m.index + m[0].length]);
243
+ }
244
+
245
+ const lines = content.split('\n');
246
+ let offset = 0;
247
+ for (const line of lines) {
248
+ const lineEnd = offset + line.length;
249
+ const inBlock = blockRanges.some(([s, e]) => offset < e && lineEnd > s);
250
+ if (!inBlock) collectFromPugLine(line, out);
251
+ offset = lineEnd + 1; // +1 for the newline
252
+ }
253
+ return out;
254
+ }
255
+
256
+ function collectFromPugLine(line, into) {
257
+ const trimmed = line.trimStart();
258
+ if (!trimmed) return;
259
+ const first = trimmed[0];
260
+ if (first === '|' || first === '<') return;
261
+ if (trimmed.startsWith('//')) return;
262
+ if (first === '+' || first === ':') return;
263
+
264
+ let i = 0;
265
+ while (i < line.length && /\s/.test(line[i])) i++;
266
+
267
+ // Optional element tag.
268
+ if (i < line.length && /[a-zA-Z]/.test(line[i])) {
269
+ let j = i;
270
+ while (j < line.length && /[\w-]/.test(line[j])) j++;
271
+ i = j;
272
+ }
273
+
274
+ // `.class` / `#id` segments. Mirrors `rewritePugLine` so the two stay
275
+ // in sync — both must accept the same chained-shorthand grammar.
276
+ while (i < line.length && (line[i] === '.' || line[i] === '#')) {
277
+ const sep = line[i];
278
+ let j = i + 1;
279
+ if (sep === '#') {
280
+ while (j < line.length && /[\w-]/.test(line[j])) j++;
281
+ } else {
282
+ while (j < line.length && /[\w/:-]/.test(line[j])) j++;
283
+ while (j < line.length && line[j] === '.' && /\d/.test(line[j + 1] || '')) {
284
+ j++;
285
+ while (j < line.length && /\d/.test(line[j])) j++;
286
+ }
287
+ }
288
+ if (j === i + 1) break;
289
+ if (sep === '.') into.add(line.slice(i + 1, j));
290
+ i = j;
291
+ }
292
+
293
+ // `(attrs)` block — pull class="..." and class!="..." values too. Pug
294
+ // chained shorthand often coexists with a `(class="...")` attribute on
295
+ // the same line (especially after `rewritePugClasses` routes special
296
+ // chars there). Capturing both lets a single pass cover the full set.
297
+ if (line[i] === '(') {
298
+ const close = findMatchingParen(line, i);
299
+ if (close !== -1) {
300
+ const attrs = line.slice(i + 1, close);
301
+ const re = /(?:^|\s)class\s*!?=\s*"([^"]*)"/g;
302
+ let am;
303
+ while ((am = re.exec(attrs)) !== null) {
304
+ for (const tok of am[1].split(/\s+/)) {
305
+ if (tok) into.add(tok);
306
+ }
307
+ }
308
+ }
309
+ }
310
+ }
311
+
208
312
  function mergeClassIntoAttrs(attrsStr, classesToAdd) {
209
313
  const classStr = classesToAdd.join(' ');
210
314
  if (!attrsStr) return `(class="${classStr}")`;
@@ -267,7 +371,10 @@ function transformIfChains(content) {
267
371
  const elseIfRe = ELSEIF_RE_TPL(chainIndent);
268
372
  const elseRe = ELSE_RE_TPL(chainIndent);
269
373
 
270
- out.push(`${chainIndent}| {#if ${ifExpr}}`);
374
+ // Collect each branch's header + body, then recurse on the body so
375
+ // nested `+if`/`+else` chains resolve. Mirrors `transformSnippets`.
376
+ /** @type {{ header: string; body: string[] }[]} */
377
+ const branches = [{ header: `${chainIndent}| {#if ${ifExpr}}`, body: [] }];
271
378
  i++;
272
379
 
273
380
  while (i < lines.length) {
@@ -275,19 +382,22 @@ function transformIfChains(content) {
275
382
 
276
383
  const eIfM = cur.match(elseIfRe);
277
384
  if (eIfM) {
278
- out.push(`${chainIndent}| {:else if ${stripQuotes(eIfM[1])}}`);
385
+ branches.push({
386
+ header: `${chainIndent}| {:else if ${stripQuotes(eIfM[1])}}`,
387
+ body: []
388
+ });
279
389
  i++;
280
390
  continue;
281
391
  }
282
392
  const eM = cur.match(elseRe);
283
393
  if (eM) {
284
- out.push(`${chainIndent}| {:else}`);
394
+ branches.push({ header: `${chainIndent}| {:else}`, body: [] });
285
395
  i++;
286
396
  continue;
287
397
  }
288
398
 
289
399
  if (cur.trim() === '') {
290
- out.push(cur);
400
+ branches[branches.length - 1].body.push(cur);
291
401
  i++;
292
402
  continue;
293
403
  }
@@ -295,7 +405,8 @@ function transformIfChains(content) {
295
405
  const lineIndent = cur.match(/^(\s*)/)[1];
296
406
  if (lineIndent.length > chainIndent.length) {
297
407
  // 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);
408
+ const deindented = cur.startsWith(indentDiff) ? cur.slice(indentDiff.length) : cur;
409
+ branches[branches.length - 1].body.push(deindented);
299
410
  i++;
300
411
  continue;
301
412
  }
@@ -303,6 +414,10 @@ function transformIfChains(content) {
303
414
  break;
304
415
  }
305
416
 
417
+ for (const b of branches) {
418
+ out.push(b.header);
419
+ if (b.body.length > 0) out.push(transformIfChains(b.body.join('\n')));
420
+ }
306
421
  out.push(`${chainIndent}| {/if}`);
307
422
  }
308
423