brand-intent 0.15.0 → 0.17.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 +1 -1
- package/parser/brandParser.ts +126 -4
- package/parser/colorSystem.ts +17 -0
- package/parser/index.ts +2 -1
- package/parser/types.ts +2 -0
- package/vscode-extension/syntaxes/brand.tmLanguage.json +12 -2
package/package.json
CHANGED
package/parser/brandParser.ts
CHANGED
|
@@ -116,6 +116,27 @@ export interface ParsedBrand {
|
|
|
116
116
|
framings?: ParsedFramingDef[];
|
|
117
117
|
/** Camera defaults (`camera` block). Absent → rear camera, no grid. */
|
|
118
118
|
camera?: ParsedCameraDef;
|
|
119
|
+
/** Photo treatment (`photo-look` block). Absent → photos stay as taken. */
|
|
120
|
+
photoLook?: ParsedPhotoLookDef;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type PhotoLookKey = 'saturation' | 'contrast' | 'warmth' | 'fade' | 'mono' | 'duotone';
|
|
124
|
+
|
|
125
|
+
export interface ParsedPhotoLookDef {
|
|
126
|
+
/** -100..100, 0 = unchanged. */
|
|
127
|
+
saturation: number;
|
|
128
|
+
/** -100..100, 0 = unchanged. */
|
|
129
|
+
contrast: number;
|
|
130
|
+
/** -100 (cool) .. 100 (warm), 0 = unchanged. */
|
|
131
|
+
warmth: number;
|
|
132
|
+
/** Lifted black point, 0..100. */
|
|
133
|
+
fade: number;
|
|
134
|
+
/** Black-and-white. */
|
|
135
|
+
mono: boolean;
|
|
136
|
+
/** Map luminance onto two colors (resolved hex), shadows → highlights. Excludes `mono`. */
|
|
137
|
+
duotone?: { shadow: string; highlight: string };
|
|
138
|
+
/** Keys the person cannot change. All others are starting values. */
|
|
139
|
+
fixed: PhotoLookKey[];
|
|
119
140
|
}
|
|
120
141
|
|
|
121
142
|
export interface ParsedCameraDef {
|
|
@@ -250,6 +271,14 @@ const VALID_STRATEGIES = new Set<PaletteStrategy>(['monochrome', 'duotone', 'tri
|
|
|
250
271
|
const VALID_ADHERENCES = new Set<Adherence>(['strict', 'on-brand', 'flexible']);
|
|
251
272
|
const TONE_CLASSES = new Set<ToneClass>(['light', 'mid', 'dark']);
|
|
252
273
|
const FAMILY_ROLES = new Set<FamilyRole>(['core', 'accent', 'contrast']);
|
|
274
|
+
|
|
275
|
+
const DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
276
|
+
function isMonthDay(value: string): boolean {
|
|
277
|
+
const m = value.match(/^(\d{2})-(\d{2})$/);
|
|
278
|
+
if (!m) return false;
|
|
279
|
+
const month = Number(m[1]), day = Number(m[2]);
|
|
280
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= DAYS_IN_MONTH[month - 1];
|
|
281
|
+
}
|
|
253
282
|
const FAMILY_ORDERS = new Set<FamilyOrder>(['cycle', 'sequence']);
|
|
254
283
|
const FAMILY_BORROWS = new Set<FamilyBorrow>(['previous', 'next', 'both', 'none']);
|
|
255
284
|
const PAIR_KEYS = ['ground', 'figure', 'text'] as const;
|
|
@@ -372,7 +401,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
372
401
|
tokensSource: 'figma',
|
|
373
402
|
};
|
|
374
403
|
|
|
375
|
-
type ActiveBlock = 'none' | 'brand-colors' | 'state-colors' | 'color-bounds' | 'color-adherence' | 'color-rules' | 'color-families' | 'theme' | 'spacing' | 'divider' | 'typography' | 'font' | 'logo' | 'photo' | 'video' | 'illustration' | 'label' | 'cta' | 'icons' | 'counter' | 'framing' | 'camera';
|
|
404
|
+
type ActiveBlock = 'none' | 'brand-colors' | 'state-colors' | 'color-bounds' | 'color-adherence' | 'color-rules' | 'color-families' | 'theme' | 'spacing' | 'divider' | 'typography' | 'font' | 'logo' | 'photo' | 'video' | 'illustration' | 'label' | 'cta' | 'icons' | 'counter' | 'framing' | 'camera' | 'photo-look';
|
|
376
405
|
let activeBlock: ActiveBlock = 'none';
|
|
377
406
|
let currentThemeName = '';
|
|
378
407
|
let currentDividerName = '';
|
|
@@ -395,7 +424,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
395
424
|
const adherenceOverridesRaw: Record<string, string> = {};
|
|
396
425
|
const colorRulesRaw: Record<string, string> = {};
|
|
397
426
|
let colorRulesDeclared = false;
|
|
398
|
-
type RawFamily = { name: string; line: number; colors: Array<{ role: string; value: string; line: number }>; pairs: Array<{ ground: string; figure: string; line: number }> };
|
|
427
|
+
type RawFamily = { name: string; line: number; from?: string; colors: Array<{ role: string; value: string; line: number }>; pairs: Array<{ ground: string; figure: string; line: number }> };
|
|
399
428
|
const familiesProps: Record<string, string> = {};
|
|
400
429
|
const familiesRaw: RawFamily[] = [];
|
|
401
430
|
let familiesDeclared = false;
|
|
@@ -421,6 +450,8 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
421
450
|
let counterDeclared = false;
|
|
422
451
|
const cameraRaw: Record<string, { value: string; line: number }> = {};
|
|
423
452
|
let cameraDeclared = false;
|
|
453
|
+
const photoLookRaw: Record<string, { value: string; line: number }> = {};
|
|
454
|
+
let photoLookDeclared = false;
|
|
424
455
|
const framingsRaw: Array<{ id: string; line: number; props: Record<string, { value: string; line: number }> }> = [];
|
|
425
456
|
|
|
426
457
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -653,8 +684,16 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
653
684
|
}
|
|
654
685
|
const colonIdx = trimmed.indexOf(':');
|
|
655
686
|
const key = colonIdx !== -1 ? trimmed.slice(0, colonIdx).trim() : trimmed;
|
|
687
|
+
if (key === 'from' && colonIdx !== -1) {
|
|
688
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
689
|
+
if (currentFamily.from !== undefined) throw new Error(`${fileName}:${lineNum}: family "${currentFamily.name}" declares from: twice`);
|
|
690
|
+
if (!isMonthDay(value)) throw new Error(`${fileName}:${lineNum}: family "${currentFamily.name}" from: must be "MM-DD" (e.g. 12-01), got "${value}"`);
|
|
691
|
+
inPairs = false;
|
|
692
|
+
currentFamily.from = value;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
656
695
|
if (colonIdx === -1 || !FAMILY_ROLES.has(key as FamilyRole)) {
|
|
657
|
-
throw new Error(`${fileName}:${lineNum}: family "${currentFamily.name}": expected core / accent / contrast / pairs, got "${trimmed}"`);
|
|
696
|
+
throw new Error(`${fileName}:${lineNum}: family "${currentFamily.name}": expected from / core / accent / contrast / pairs, got "${trimmed}"`);
|
|
658
697
|
}
|
|
659
698
|
inPairs = false;
|
|
660
699
|
currentFamily.colors.push({ role: key, value: trimmed.slice(colonIdx + 1).trim(), line: lineNum });
|
|
@@ -801,6 +840,18 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
801
840
|
cameraRaw[entry.slice(0, colonIdx).trim()] = { value: entry.slice(colonIdx + 1).trim(), line: lineNum };
|
|
802
841
|
continue;
|
|
803
842
|
}
|
|
843
|
+
if (activeBlock === 'photo-look') {
|
|
844
|
+
const trimmed = raw.trim();
|
|
845
|
+
if (trimmed.startsWith('#')) continue;
|
|
846
|
+
// `# …` is a comment, but `#RGB` / `#RRGGBB` is a duotone color
|
|
847
|
+
const commentIdx = trimmed.search(/\s+#(?![0-9a-f]{3}(?:[0-9a-f]{3}(?:[0-9a-f]{2})?)?(?:\s|$))/i);
|
|
848
|
+
const entry = commentIdx !== -1 ? trimmed.slice(0, commentIdx) : trimmed;
|
|
849
|
+
const colonIdx = entry.indexOf(':');
|
|
850
|
+
// Bare flag (`mono`) or KEY: VALUE
|
|
851
|
+
if (colonIdx === -1) photoLookRaw[entry.trim()] = { value: '', line: lineNum };
|
|
852
|
+
else photoLookRaw[entry.slice(0, colonIdx).trim()] = { value: entry.slice(colonIdx + 1).trim(), line: lineNum };
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
804
855
|
if (activeBlock === 'framing') {
|
|
805
856
|
const trimmed = raw.trim();
|
|
806
857
|
if (trimmed.startsWith('#')) continue;
|
|
@@ -881,6 +932,14 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
881
932
|
continue;
|
|
882
933
|
}
|
|
883
934
|
|
|
935
|
+
// photo-look — photo treatment (saturation)
|
|
936
|
+
if (raw.trim() === 'photo-look') {
|
|
937
|
+
if (photoLookDeclared) throw new Error(`${fileName}:${lineNum}: duplicate photo-look block`);
|
|
938
|
+
photoLookDeclared = true;
|
|
939
|
+
activeBlock = 'photo-look';
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
|
|
884
943
|
// framing <id> — photo framing module (horizon position)
|
|
885
944
|
const framingMatch = raw.match(/^framing\s+(\S+)\s*$/);
|
|
886
945
|
if (framingMatch) {
|
|
@@ -1150,7 +1209,16 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1150
1209
|
}
|
|
1151
1210
|
brandColors[c.key] = c.hex;
|
|
1152
1211
|
}
|
|
1153
|
-
families.push({ name: raw.name, colors, pairs: [] });
|
|
1212
|
+
families.push({ name: raw.name, ...(raw.from ? { from: raw.from } : {}), colors, pairs: [] });
|
|
1213
|
+
}
|
|
1214
|
+
const dated = familiesRaw.filter(f => f.from !== undefined);
|
|
1215
|
+
if (dated.length > 0 && dated.length < familiesRaw.length) {
|
|
1216
|
+
const missing = familiesRaw.filter(f => f.from === undefined).map(f => f.name).join(', ');
|
|
1217
|
+
throw new Error(`${fileName}: color-families: from: must be declared on every family or on none (missing: ${missing})`);
|
|
1218
|
+
}
|
|
1219
|
+
for (const f of dated) {
|
|
1220
|
+
const twin = dated.find(o => o !== f && o.from === f.from);
|
|
1221
|
+
if (twin) throw new Error(`${fileName}:${f.line}: families "${twin.name}" and "${f.name}" share from: ${f.from}`);
|
|
1154
1222
|
}
|
|
1155
1223
|
colorFamilies = { order, borrow, distance, shared, families };
|
|
1156
1224
|
if (borrow !== 'none' && reachCoversAll(families.length, { order, borrow, distance })) {
|
|
@@ -1557,6 +1625,60 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
1557
1625
|
result.camera = camera;
|
|
1558
1626
|
}
|
|
1559
1627
|
|
|
1628
|
+
// Finalise photo treatment
|
|
1629
|
+
if (photoLookDeclared) {
|
|
1630
|
+
const keys: PhotoLookKey[] = ['saturation', 'contrast', 'warmth', 'fade', 'mono', 'duotone'];
|
|
1631
|
+
for (const [key, { line }] of Object.entries(photoLookRaw)) {
|
|
1632
|
+
if (key !== 'fixed' && !keys.includes(key as PhotoLookKey)) throw new Error(`${fileName}:${line}: unknown photo-look property "${key}"`);
|
|
1633
|
+
}
|
|
1634
|
+
const int = (key: string, min: number, max: number): number => {
|
|
1635
|
+
const p = photoLookRaw[key];
|
|
1636
|
+
if (!p) return 0;
|
|
1637
|
+
const n = Number(p.value);
|
|
1638
|
+
if (!/^[+-]?\d+$/.test(p.value) || n < min || n > max) {
|
|
1639
|
+
throw new Error(`${fileName}:${p.line}: photo-look ${key} must be an integer from ${min} to ${max}, got "${p.value}"`);
|
|
1640
|
+
}
|
|
1641
|
+
return n;
|
|
1642
|
+
};
|
|
1643
|
+
const look: ParsedPhotoLookDef = {
|
|
1644
|
+
saturation: int('saturation', -100, 100),
|
|
1645
|
+
contrast: int('contrast', -100, 100),
|
|
1646
|
+
warmth: int('warmth', -100, 100),
|
|
1647
|
+
fade: int('fade', 0, 100),
|
|
1648
|
+
mono: false,
|
|
1649
|
+
fixed: [],
|
|
1650
|
+
};
|
|
1651
|
+
const mono = photoLookRaw['mono'];
|
|
1652
|
+
if (mono) {
|
|
1653
|
+
if (mono.value !== '') throw new Error(`${fileName}:${mono.line}: photo-look mono is a flag and takes no value`);
|
|
1654
|
+
look.mono = true;
|
|
1655
|
+
}
|
|
1656
|
+
const duo = photoLookRaw['duotone'];
|
|
1657
|
+
if (duo) {
|
|
1658
|
+
if (mono) throw new Error(`${fileName}:${duo.line}: photo-look cannot be both mono and duotone`);
|
|
1659
|
+
const refs = duo.value.split(/\s+/);
|
|
1660
|
+
if (refs.length !== 2) throw new Error(`${fileName}:${duo.line}: photo-look duotone must be "SHADOW HIGHLIGHT", got "${duo.value}"`);
|
|
1661
|
+
const [shadow, highlight] = refs.map(ref => {
|
|
1662
|
+
if (ref.startsWith('$') && !brandColors[ref.slice(1)]) {
|
|
1663
|
+
throw new Error(`${fileName}:${duo.line}: photo-look duotone references unknown brand color "${ref}"`);
|
|
1664
|
+
}
|
|
1665
|
+
const hex = normalizeHex(ref.startsWith('$') ? brandColors[ref.slice(1)] : ref);
|
|
1666
|
+
if (!hex) throw new Error(`${fileName}:${duo.line}: photo-look duotone colors must be $ref or #HEX, got "${ref}"`);
|
|
1667
|
+
return hex;
|
|
1668
|
+
});
|
|
1669
|
+
look.duotone = { shadow, highlight };
|
|
1670
|
+
}
|
|
1671
|
+
const fixed = photoLookRaw['fixed'];
|
|
1672
|
+
if (fixed) {
|
|
1673
|
+
for (const key of fixed.value.split(/\s+/)) {
|
|
1674
|
+
if (!keys.includes(key as PhotoLookKey)) throw new Error(`${fileName}:${fixed.line}: photo-look fixed lists unknown key "${key}"`);
|
|
1675
|
+
if (!photoLookRaw[key]) throw new Error(`${fileName}:${fixed.line}: photo-look fixed lists "${key}", which is not set`);
|
|
1676
|
+
look.fixed.push(key as PhotoLookKey);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
result.photoLook = look;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1560
1682
|
// Finalise framing modules
|
|
1561
1683
|
if (framingsRaw.length > 0) {
|
|
1562
1684
|
const known = new Set(['label', 'horizon', 'description']);
|
package/parser/colorSystem.ts
CHANGED
|
@@ -193,6 +193,23 @@ export function colorOrigin(
|
|
|
193
193
|
return null;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Family in season on `date`: the one with the latest `from:` on or before that
|
|
198
|
+
* month/day, wrapping to the year's last start before the first one. Uses the
|
|
199
|
+
* date's local calendar day. -1 when the families declare no dates.
|
|
200
|
+
*/
|
|
201
|
+
export function familyIndexForDate(families: Pick<ColorFamily, 'from'>[], date: Date): number {
|
|
202
|
+
if (families.length === 0 || families.some(f => !f.from)) return -1;
|
|
203
|
+
const today = `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
204
|
+
let best = -1;
|
|
205
|
+
let latest = -1;
|
|
206
|
+
families.forEach((f, i) => {
|
|
207
|
+
if (f.from! <= today && (best === -1 || f.from! > families[best].from!)) best = i;
|
|
208
|
+
if (latest === -1 || f.from! > families[latest].from!) latest = i;
|
|
209
|
+
});
|
|
210
|
+
return best !== -1 ? best : latest;
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
/** Name of the family that declares `hex` (first match), for error messages. */
|
|
197
214
|
export function owningFamily(hex: string, families: ColorFamily[]): string | undefined {
|
|
198
215
|
return families.find(f => f.colors.some(c => c.hex === hex))?.name;
|
package/parser/index.ts
CHANGED
|
@@ -20,10 +20,11 @@ export {
|
|
|
20
20
|
familyTextCandidates,
|
|
21
21
|
checkFamilyPair,
|
|
22
22
|
enumerateFamilyPairs,
|
|
23
|
+
familyIndexForDate,
|
|
23
24
|
DEFAULT_COLOR_RULES,
|
|
24
25
|
} from './colorSystem';
|
|
25
26
|
export type { ReachConfig } from './colorSystem';
|
|
26
|
-
export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef, BrandColorPrintSpec, ParsedAssetDef, ParsedBadgeStyle, LogoAlign, ParsedCounterDef, CounterFormat, CounterShape, ParsedFramingDef, ParsedCameraDef } from './brandParser';
|
|
27
|
+
export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef, BrandColorPrintSpec, ParsedAssetDef, ParsedBadgeStyle, LogoAlign, ParsedCounterDef, CounterFormat, CounterShape, ParsedFramingDef, ParsedCameraDef, ParsedPhotoLookDef, PhotoLookKey } from './brandParser';
|
|
27
28
|
|
|
28
29
|
export { parseIconRef, parseIconSource, resolveIconRef, parseListItem, phosphorName, PHOSPHOR_WEIGHTS } from './icons';
|
|
29
30
|
export type { IconRef, IconSource, ParsedIconDef, ResolvedIcon, PhosphorWeight } from './icons';
|
package/parser/types.ts
CHANGED
|
@@ -156,6 +156,8 @@ export interface FamilyPair extends PairCheck {
|
|
|
156
156
|
|
|
157
157
|
export interface ColorFamily {
|
|
158
158
|
name: string;
|
|
159
|
+
/** Start date `MM-DD` (`from:`). Declared on every family or on none. */
|
|
160
|
+
from?: string;
|
|
159
161
|
colors: FamilyColor[];
|
|
160
162
|
pairs: FamilyPair[];
|
|
161
163
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
{ "include": "#color-rules-entry" },
|
|
16
16
|
{ "include": "#color-families-property" },
|
|
17
17
|
{ "include": "#family-header" },
|
|
18
|
+
{ "include": "#family-from" },
|
|
18
19
|
{ "include": "#family-role" },
|
|
19
20
|
{ "include": "#family-pairs-keyword" },
|
|
20
21
|
{ "include": "#color-mode-enum" },
|
|
@@ -143,6 +144,15 @@
|
|
|
143
144
|
"5": { "name": "string.unquoted.brand-intent-brand" }
|
|
144
145
|
}
|
|
145
146
|
},
|
|
147
|
+
"family-from": {
|
|
148
|
+
"comment": "Family start date: from: MM-DD",
|
|
149
|
+
"match": "^(\\s{4,})(from)(:\\s*)(\\d{2}-\\d{2})\\s*$",
|
|
150
|
+
"captures": {
|
|
151
|
+
"2": { "name": "support.type.property-name.brand-intent-brand" },
|
|
152
|
+
"3": { "name": "punctuation.separator.key-value.brand-intent-brand" },
|
|
153
|
+
"4": { "name": "constant.numeric.brand-intent-brand" }
|
|
154
|
+
}
|
|
155
|
+
},
|
|
146
156
|
"family-pairs-keyword": {
|
|
147
157
|
"match": "^(\\s{4,})(pairs)\\s*$",
|
|
148
158
|
"captures": {
|
|
@@ -389,8 +399,8 @@
|
|
|
389
399
|
}
|
|
390
400
|
},
|
|
391
401
|
"icons-counter-header": {
|
|
392
|
-
"comment": "Bare `icons` / `counter` block headers.",
|
|
393
|
-
"match": "^(icons|counter)\\s*$",
|
|
402
|
+
"comment": "Bare `icons` / `counter` / `camera` / `photo-look` block headers.",
|
|
403
|
+
"match": "^(icons|counter|camera|photo-look)\\s*$",
|
|
394
404
|
"captures": {
|
|
395
405
|
"1": { "name": "keyword.control.brand-intent-brand" }
|
|
396
406
|
}
|