brand-intent 0.8.0 → 0.9.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.8.0",
3
+ "version": "0.9.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",
@@ -34,7 +34,7 @@
34
34
  ],
35
35
  "scripts": {
36
36
  "release": "./scripts/release.sh",
37
- "test": "tsx tests/brandParser.test.ts && tsx tests/realBrands.test.ts"
37
+ "test": "tsx tests/brandParser.test.ts && tsx tests/identityParser.test.ts && tsx tests/realBrands.test.ts"
38
38
  },
39
39
  "devDependencies": {
40
40
  "tsx": "^4.19.0"
@@ -91,6 +91,12 @@ export interface ParsedAssetDef {
91
91
  lottieSrc?: string;
92
92
  animated?: boolean;
93
93
  loop?: boolean;
94
+ /** Path to a glTF/GLB 3D model. When present, the asset renders as a live 3D object; `src` remains the 2D fallback. */
95
+ gltfSrc?: string;
96
+ /** Procedural generator id (e.g. "mobius", "spiral", "ribbon"). Renders as live geometry; `src` is the 2D fallback. */
97
+ generator?: string;
98
+ /** Default parameters for the procedural generator, keyed by param name. Declared via the indented `generator-params` sub-block. */
99
+ generatorParams?: Record<string, number>;
94
100
  }
95
101
 
96
102
  export interface ParsedBadgeStyle {
@@ -312,6 +318,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
312
318
  const videosRaw: Array<{ id: string; props: Record<string, string> }> = [];
313
319
  const illustrationsRaw: Array<{ id: string; props: Record<string, string> }> = [];
314
320
  let currentAssetId = '';
321
+ let currentSubBlock: string | null = null;
315
322
  const labelRaw: Record<string, string> = {};
316
323
  const ctaRaw: Record<string, string> = {};
317
324
 
@@ -569,19 +576,33 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
569
576
  continue;
570
577
  }
571
578
  if ((activeBlock === 'logo' || activeBlock === 'photo' || activeBlock === 'video' || activeBlock === 'illustration') && currentAssetId) {
579
+ const indent = raw.match(/^(\s*)/)?.[1].length ?? 0;
572
580
  const colonIdx = raw.indexOf(':');
573
581
  if (colonIdx !== -1) {
574
582
  const key = raw.slice(0, colonIdx).trim();
575
583
  const value = raw.slice(colonIdx + 1).trim();
576
584
  const arr = activeBlock === 'logo' ? logosRaw : activeBlock === 'photo' ? photosRaw : activeBlock === 'video' ? videosRaw : illustrationsRaw;
577
585
  const last = arr[arr.length - 1];
578
- if (last && last.id === currentAssetId) last.props[key] = value;
586
+ if (last && last.id === currentAssetId) {
587
+ // 4+ space indent after a bare sub-block keyword (e.g. generator-params) → nested param.
588
+ // Store under "subBlock.key" so toAssetDef can split it out.
589
+ if (indent >= 4 && currentSubBlock) {
590
+ last.props[`${currentSubBlock}.${key}`] = value;
591
+ } else {
592
+ last.props[key] = value;
593
+ currentSubBlock = null;
594
+ }
595
+ }
579
596
  } else {
580
597
  const trimmed = raw.trim();
581
598
  if (trimmed === 'animated' || trimmed === 'loop') {
582
599
  const arr = activeBlock === 'logo' ? logosRaw : activeBlock === 'photo' ? photosRaw : activeBlock === 'video' ? videosRaw : illustrationsRaw;
583
600
  const last = arr[arr.length - 1];
584
601
  if (last && last.id === currentAssetId) last.props[trimmed] = 'true';
602
+ currentSubBlock = null;
603
+ } else if (trimmed === 'generator-params') {
604
+ // Open a nested sub-block — subsequent deeper-indented key: value lines attach here.
605
+ currentSubBlock = 'generator-params';
585
606
  }
586
607
  }
587
608
  continue;
@@ -986,6 +1007,18 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
986
1007
  if (raw.props['lottie']) def.lottieSrc = raw.props['lottie'];
987
1008
  if (raw.props['animated'] === 'true') def.animated = true;
988
1009
  if (raw.props['loop'] === 'true') def.loop = true;
1010
+ if (raw.props['gltf']) def.gltfSrc = raw.props['gltf'];
1011
+ if (raw.props['generator']) def.generator = raw.props['generator'];
1012
+ // generator-params are stored as "generator-params.KEY" by the sub-block parser.
1013
+ const paramEntries: [string, number][] = [];
1014
+ for (const [k, v] of Object.entries(raw.props)) {
1015
+ if (k.startsWith('generator-params.')) {
1016
+ const paramKey = k.slice('generator-params.'.length);
1017
+ const n = parseFloat(v);
1018
+ if (!Number.isNaN(n)) paramEntries.push([paramKey, n]);
1019
+ }
1020
+ }
1021
+ if (paramEntries.length > 0) def.generatorParams = Object.fromEntries(paramEntries);
989
1022
  return def;
990
1023
  };
991
1024
  if (logosRaw.length > 0 || photosRaw.length > 0 || videosRaw.length > 0 || illustrationsRaw.length > 0) {
@@ -26,6 +26,8 @@ export interface ParsedIdentity {
26
26
  tagline?: string;
27
27
  /** Competitive positioning — what makes this brand the only one of its kind */
28
28
  positioning?: string;
29
+ /** Brand purpose — why the brand exists, the inner motivation (distinct from promise, mission, vision) */
30
+ purpose?: string;
29
31
  /** Organizational mission — what the brand does, for whom */
30
32
  mission?: string;
31
33
  /** Aspirational future state */
@@ -109,6 +111,7 @@ export function parseIdentityFile(content: string, fileName: string): ParsedIden
109
111
  let promise = '';
110
112
  let tagline = '';
111
113
  let positioning = '';
114
+ let purpose = '';
112
115
  let mission = '';
113
116
  let vision = '';
114
117
  let archetype = '';
@@ -195,6 +198,7 @@ export function parseIdentityFile(content: string, fileName: string): ParsedIden
195
198
  if (continuationKey === 'essence') essence += ' ' + continuation;
196
199
  else if (continuationKey === 'promise') promise += ' ' + continuation;
197
200
  else if (continuationKey === 'positioning') positioning += ' ' + continuation;
201
+ else if (continuationKey === 'purpose') purpose += ' ' + continuation;
198
202
  else if (continuationKey === 'mission') mission += ' ' + continuation;
199
203
  else if (continuationKey === 'vision') vision += ' ' + continuation;
200
204
  continue;
@@ -300,6 +304,10 @@ export function parseIdentityFile(content: string, fileName: string): ParsedIden
300
304
  positioning = val;
301
305
  continuationKey = 'positioning';
302
306
  break;
307
+ case 'purpose':
308
+ purpose = val;
309
+ continuationKey = 'purpose';
310
+ break;
303
311
  case 'mission':
304
312
  mission = val;
305
313
  continuationKey = 'mission';
@@ -425,6 +433,7 @@ export function parseIdentityFile(content: string, fileName: string): ParsedIden
425
433
  // Optional fields — only include if present
426
434
  if (tagline) result.tagline = tagline;
427
435
  if (positioning) result.positioning = positioning;
436
+ if (purpose) result.purpose = purpose;
428
437
  if (mission) result.mission = mission;
429
438
  if (vision) result.vision = vision;
430
439
  if (archetype) result.archetype = archetype;