brand-intent 0.3.1 → 0.4.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brand-intent",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "An open file format specification for making brand intent machine-readable.",
5
5
  "author": "Christophe Stoll <christophe@stoll.studio>",
6
6
  "license": "MIT",
@@ -22,6 +22,26 @@ export interface ParsedImageRegion {
22
22
  colEnd?: number; // 0-based column (undefined = full width)
23
23
  }
24
24
 
25
+ /** Anchor keywords for illustration placement. */
26
+ export type IllustrationAnchor =
27
+ | 'top-left' | 'top-center' | 'top-right'
28
+ | 'center-left' | 'center' | 'center-right'
29
+ | 'bottom-left' | 'bottom-center' | 'bottom-right';
30
+
31
+ /** Parsed illustration placement from `illustration:` line. */
32
+ export interface ParsedIllustration {
33
+ anchor: IllustrationAnchor;
34
+ fit: 'width' | 'height';
35
+ /** Scale as fraction 0–1 (e.g. 0.9 = 90% of canvas dimension). */
36
+ scale: number;
37
+ }
38
+
39
+ const ILLUSTRATION_ANCHORS = new Set<string>([
40
+ 'top-left', 'top-center', 'top-right',
41
+ 'center-left', 'center', 'center-right',
42
+ 'bottom-left', 'bottom-center', 'bottom-right',
43
+ ]);
44
+
25
45
  export interface ParsedComposition {
26
46
  /** Programmatic ID (from `id:` line, or fallback from first `#` line). */
27
47
  id: string;
@@ -44,6 +64,8 @@ export interface ParsedComposition {
44
64
  sticky: ParsedStickyRegion[];
45
65
  /** Flow regions — content-driven flow areas. */
46
66
  flow: ParsedFlowRegion | null;
67
+ /** Illustration placement (grid mode only for now). */
68
+ illustration?: ParsedIllustration;
47
69
  }
48
70
 
49
71
  /** A sticky (repetition) region — composed elements that don't participate in flow. */
@@ -128,8 +150,6 @@ export interface ParsedDivider {
128
150
 
129
151
  // ── Constants ─────────────────────────────────────────────────────────────────
130
152
 
131
- /** Backward-compat map: band letters A-F → 0-based row indices */
132
- const LEGACY_BAND_MAP: Record<string, number> = { A: 0, B: 1, C: 2, D: 3, E: 4, F: 5 };
133
153
  const GRID_Y_KEYWORDS = new Set<string>(['top', 'center', 'bottom']);
134
154
 
135
155
  const VALID_SLOTS = new Set<string>(['primary', 'secondary', 'detail', 'meta', 'cta', 'label']);
@@ -181,6 +201,7 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
181
201
  const slots: ParsedSlot[] = [];
182
202
  const sticky: ParsedStickyRegion[] = [];
183
203
  let flow: ParsedFlowRegion | null = null;
204
+ let illustration: ParsedIllustration | undefined = undefined;
184
205
 
185
206
  /** Parse a fraction string like "2/3" → 0.667. */
186
207
  function parseFraction(s: string, ctx: string): number {
@@ -262,6 +283,23 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
262
283
  continue;
263
284
  }
264
285
 
286
+ // Illustration placement: "illustration: bottom-center fit-width 90%"
287
+ if (line.startsWith('illustration:')) {
288
+ const parts = line.slice(13).trim().split(/\s+/);
289
+ if (parts.length < 3) throw new Error(`${fileName}:${lineNum}: invalid illustration line — expected "illustration: <anchor> <fit-width|fit-height> <N%>"`);
290
+ const anchor = parts[0];
291
+ if (!ILLUSTRATION_ANCHORS.has(anchor)) throw new Error(`${fileName}:${lineNum}: invalid illustration anchor "${anchor}" (expected ${[...ILLUSTRATION_ANCHORS].join(', ')})`);
292
+ const fitStr = parts[1];
293
+ if (fitStr !== 'fit-width' && fitStr !== 'fit-height') throw new Error(`${fileName}:${lineNum}: invalid illustration fit "${fitStr}" (expected fit-width or fit-height)`);
294
+ const scaleStr = parts[2];
295
+ const scaleMatch = scaleStr.match(/^(\d+(?:\.\d+)?)%$/);
296
+ if (!scaleMatch) throw new Error(`${fileName}:${lineNum}: invalid illustration scale "${scaleStr}" (expected e.g. "90%")`);
297
+ const scale = parseFloat(scaleMatch[1]) / 100;
298
+ if (scale <= 0 || scale > 2) throw new Error(`${fileName}:${lineNum}: illustration scale out of range (expected 1%–200%)`);
299
+ illustration = { anchor: anchor as IllustrationAnchor, fit: fitStr === 'fit-width' ? 'width' : 'height', scale };
300
+ continue;
301
+ }
302
+
265
303
  // grid: is no longer used in layout files (format owns the grid). Silently skip.
266
304
  if (line.startsWith('grid:')) {
267
305
  console.warn(`${fileName}:${lineNum}: "grid:" in layout files is no longer needed — format defines the grid`);
@@ -292,16 +330,6 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
292
330
  logo = { row: parseInt(rowMatch[1], 10) - 1, col, size: (rowMatch[3] as 's' | 'm' | 'l') ?? undefined };
293
331
  continue;
294
332
  }
295
- // Backward compat: "logo: band A [col] X"
296
- const bandMatch = line.match(/^logo:\s+band\s+([A-F])\s+(?:col\s+)?(left|center|right)(?:\s+(s|m|l))?\s*$/);
297
- if (bandMatch) {
298
- const row = LEGACY_BAND_MAP[bandMatch[1]];
299
- if (row === undefined) throw new Error(`${fileName}:${lineNum}: unknown band "${bandMatch[1]}"`);
300
- console.warn(`${fileName}:${lineNum}: deprecated "band ${bandMatch[1]}" syntax — use "row ${row + 1}" instead`);
301
- const col = resolveColKeyword(bandMatch[2], fileName, lineNum);
302
- logo = { row, col, size: (bandMatch[3] as 's' | 'm' | 'l') ?? undefined };
303
- continue;
304
- }
305
333
  throw new Error(`${fileName}:${lineNum}: invalid logo line "${line}" (expected "logo: row N left|center|right" or "logo: top/center/bottom left|center|right")`);
306
334
  }
307
335
 
@@ -319,15 +347,6 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
319
347
  icon = { row: parseInt(rowMatch[1], 10) - 1, col, size: (rowMatch[3] as 's' | 'm' | 'l') ?? undefined };
320
348
  continue;
321
349
  }
322
- const bandMatch = line.match(/^icon:\s+band\s+([A-F])\s+(?:col\s+)?(left|center|right)(?:\s+(s|m|l))?\s*$/);
323
- if (bandMatch) {
324
- const row = LEGACY_BAND_MAP[bandMatch[1]];
325
- if (row === undefined) throw new Error(`${fileName}:${lineNum}: unknown band "${bandMatch[1]}"`);
326
- console.warn(`${fileName}:${lineNum}: deprecated "band ${bandMatch[1]}" syntax — use "row ${row + 1}" instead`);
327
- const col = resolveColKeyword(bandMatch[2], fileName, lineNum);
328
- icon = { row, col, size: (bandMatch[3] as 's' | 'm' | 'l') ?? undefined };
329
- continue;
330
- }
331
350
  throw new Error(`${fileName}:${lineNum}: invalid icon line "${line}"`);
332
351
  }
333
352
 
@@ -511,25 +530,6 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
511
530
  continue;
512
531
  }
513
532
 
514
- // Backward compat: "primary band D cols 1--4 left"
515
- const bandSlotMatch = line.match(/^(\w+)\s+band\s+([A-F])\s+cols\s+(\S+)\s+(left|center|right)$/);
516
- if (bandSlotMatch) {
517
- const [, slotId, bandLetter, colRange, align] = bandSlotMatch;
518
- if (!VALID_SLOTS.has(slotId)) throw new Error(`${fileName}:${lineNum}: unknown slot "${slotId}"`);
519
- const row = LEGACY_BAND_MAP[bandLetter];
520
- if (row === undefined) throw new Error(`${fileName}:${lineNum}: unknown band "${bandLetter}"`);
521
- console.warn(`${fileName}:${lineNum}: deprecated "band ${bandLetter}" syntax — use "row ${row + 1}" instead`);
522
- const cols = parseColRange(colRange, fileName, lineNum, 12);
523
- slots.push({
524
- id: slotId as SlotId,
525
- row,
526
- colStart: cols.start,
527
- colEnd: cols.end,
528
- textAlign: align as 'left' | 'center' | 'right',
529
- });
530
- continue;
531
- }
532
-
533
533
  // Unknown line
534
534
  throw new Error(`${fileName}:${lineNum}: unexpected "${line}"`);
535
535
  }
@@ -552,5 +552,5 @@ export function parseCompositionFile(content: string, fileName: string): ParsedC
552
552
  // Validate required fields
553
553
  if (!image) image = 'full'; // default to full-bleed image
554
554
  if (!logo && !logoNone && !sticky.length) throw new Error(`${fileName}: missing "logo:" line (or use sticky: block)`);
555
- return { id, name, description, mode, image, imageExplicit, logo, logoNone: logoNone || undefined, icon, slots, sticky, flow };
555
+ return { id, name, description, mode, image, imageExplicit, logo, logoNone: logoNone || undefined, icon, slots, sticky, flow, illustration };
556
556
  }
@@ -29,7 +29,7 @@ export interface ParsedFormat {
29
29
  dpi?: number;
30
30
  bleedMm?: number;
31
31
  // Optional grid override — absent means auto portrait/landscape selection
32
- grid?: { bands: number; cols: number };
32
+ grid?: { rows: number; cols: number };
33
33
  // Zones — always stored in px (mm values converted during parse)
34
34
  danger: ZoneInsets;
35
35
  gridCrop: ZoneInsets | null;
@@ -183,12 +183,12 @@ export function parseFormatFile(content: string, fileName: string): ParsedFormat
183
183
 
184
184
  case 'grid': {
185
185
  const parts = value.split(/\s+/);
186
- if (parts.length !== 2) throw new Error(`${fileName}:${lineNum}: grid requires "bands cols", got "${value}"`);
187
- const bands = parseInt(parts[0], 10);
186
+ if (parts.length !== 2) throw new Error(`${fileName}:${lineNum}: grid requires "rows cols", got "${value}"`);
187
+ const rows = parseInt(parts[0], 10);
188
188
  const cols = parseInt(parts[1], 10);
189
- if (isNaN(bands) || bands < 1 || isNaN(cols) || cols < 1)
190
- throw new Error(`${fileName}:${lineNum}: grid bands and cols must be positive integers`);
191
- grid = { bands, cols };
189
+ if (isNaN(rows) || rows < 1 || isNaN(cols) || cols < 1)
190
+ throw new Error(`${fileName}:${lineNum}: grid rows and cols must be positive integers`);
191
+ grid = { rows, cols };
192
192
  break;
193
193
  }
194
194
 
package/parser/index.ts CHANGED
@@ -21,6 +21,8 @@ export type {
21
21
  ParsedStickyRegion,
22
22
  ParsedFlowItem,
23
23
  ParsedImageRegion,
24
+ ParsedIllustration,
25
+ IllustrationAnchor,
24
26
  GridYKeyword,
25
27
  } from './compositionParser';
26
28