Setting the file. One moment.
Tokens · Product Launch Video · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page export function parseFonts
— line 179
This file
Number 31.24
Position 24 of 33
Type JavaScript
Size 10 KB
Lines 218 scripts/lib/ tokens.mjs
JavaScript · 218 lines · 10 KB
of
md.
split
(
/
\r
?
\n
/
)) {
11 if ( / ^ colors: \s *$ / . test (line)) {
12 inBlock = true ;
13 continue ;
14 }
15 if ( ! inBlock) continue ;
16 if ( / ^ \S / . test (line)) break ; // dedent to a top-level key → end of block
17 const m = line. match (
18 / ^ \s + ( [\w-] + ): \s * (?:"( [ ^ "] + )" | '( [ ^ '] + )' | (# [0-9a-fA-F] {3,8}| rgba ? \( [ ^ )] * \) | [ ^ #\s][ ^ #\n] *? )) \s * (?:# . * ) ?$ / ,
19 );
20 if (m) out. push ([m[ 1 ], (m[ 2 ] ?? m[ 3 ] ?? m[ 4 ]). trim ()]);
21 }
22 return out;
23 }
24
25 // relative luminance of a #rrggbb (null for non-hex like rgba()).
26 export function lum ( v ) {
27 const m = / ^ # ? ( [0-9a-fA-F] {6} ) $ / . exec ( String (v). trim ());
28 if ( ! m) return null ;
29 const n = parseInt (m[ 1 ], 16 );
30 return 0.2126 * ((n >> 16 ) & 255 ) + 0.7152 * ((n >> 8 ) & 255 ) + 0.0722 * (n & 255 );
31 }
32
33 // chroma (max−min channel) of a #rrggbb — a cheap "how colorful" proxy; −1 for non-hex.
34 export function chroma ( v ) {
35 const m = / ^ # ? ( [0-9a-fA-F] {6} ) $ / . exec ( String (v). trim ());
36 if ( ! m) return - 1 ;
37 const n = parseInt (m[ 1 ], 16 );
38 const r = (n >> 16 ) & 255 ,
39 g = (n >> 8 ) & 255 ,
40 b = n & 255 ;
41 return Math. max (r, g, b) - Math. min (r, g, b);
42 }
43
44 // Browser user-agent default colors for links / visited links. These leak into a
45 // capture from any UNSTYLED <a> and are NOT brand colors — but being pure & saturated
46 // they beat a real accent on chroma alone. Never let one become the accent.
47 export const UA_DEFAULT_COLORS = new Set (
48 [ "#0000EE" , "#0000FF" , "#0000CC" , "#1A0DAB" , "#551A8B" , "#EE0000" ]. map (( c ) => c. toUpperCase ()),
49 );
50
51 export const ICON_FONT_PATTERN =
52 /(?: ^| [\s_-] )icons ? (?: [\s_-] |$ ) | icomoon | font \s * - ? awesome | glyphicons ?| material \s * icons | feather \s * icons | (?:icon | glyph) . * font | font . * (?:icon | glyph) |^ vidaxlfont $ / i ;
53
54 export function isIconFont ( name ) {
55 return ICON_FONT_PATTERN . test ( String (name));
56 }
57
58 // Semantic STATUS roles (green "positive", red "negative"/"error", amber "warning" …). Their HUE
59 // carries the meaning, so they are never a brand ACCENT — a status red is frequently the most
60 // chromatic color in a palette (e.g. #dc2626 chroma 182 beats a deep-blue accent #1E40AF chroma
61 // 145) and would otherwise win a pure chroma ranking, painting captions/highlights the error red.
62 // build-frame.mjs uses this same key set to protect status colors during the preset→brand remix.
63 export const STATUS_ROLE_KEY =
64 /(?: ^| [-_] )(?:positive | negative | success | error | warning | danger | good | bad | up | down | info | neutral | alert | caution | critical)(?: [-_] |$ )/ i ;
65
66 // Pick the brand ACCENT — never by raw chroma alone, never a UA-default link color.
67 // Priority:
68 // 1) with capture colorStats → the colorful color that RECURS across the UI. The brand
69 // accent shows up in MANY roles (link text + icon + button + badge), whereas a one-off
70 // CTA fill appears in just one. So rank chromatic (chroma>40) candidates by role
71 // diversity first, then total prevalence, then interactive use, then chroma. This keeps
72 // a pervasive brand color (e.g. an indigo used everywhere) ahead of a single bright
73 // button fill (e.g. a lime used once) — the old "top interactiveBg" rule picked the
74 // latter. Requiring interactiveBg>0 is dropped so a text/icon-only accent can still win.
75 // 2) no stats → most chromatic color AFTER removing UA defaults + `exclude`.
76 // A stray default link color (e.g. #0000EE) can win under neither path.
77 export function pickAccent ( stats , colors , exclude = []) {
78 const ban = new Set ([ ... exclude, ... UA_DEFAULT_COLORS ]. map (( c ) => String (c). toUpperCase ()));
79 const ok = ( h ) => / ^ # [0-9a-fA-F] {6}$ / . test ( String (h)) && ! ban. has ( String (h). toUpperCase ());
80 // Prominence rank from the (frequency-ordered) `colors` palette: index 0 = most used.
81 // A saturated color sitting at the TAIL is almost always a one-off (a single CTA fill),
82 // not the brand accent — capture colorStats counts are too sparse to tell these apart
83 // (e.g. Linear's indigo and a lime CTA both register count≈1), but palette ORDER does.
84 const rank = new Map ((colors ?? []). map (( h , i ) => [ String (h). toUpperCase (), i]));
85 const prom = ( h ) => (rank. has ( String (h). toUpperCase ()) ? rank. get ( String (h). toUpperCase ()) : 1e9 );
86 if (Array. isArray (stats) && stats. length ) {
87 const roles = ( s ) =>
88 ((s.interactiveBg || 0 ) > 0 ? 1 : 0 ) +
89 ((s.textCount || 0 ) > 0 ? 1 : 0 ) +
90 ((s.bgCount || 0 ) > 0 ? 1 : 0 );
91 const a = stats
92 . filter (( s ) => ok (s?.hex) && chroma (s.hex) > 40 )
93 . sort (
94 ( x , y ) =>
95 roles (y) - roles (x) || // used in MORE roles (link+icon+button) = the brand accent
96 prom (x.hex) - prom (y.hex) || // earlier in the palette = more prominent
97 (y.count || 0 ) - (x.count || 0 ) ||
98 (y.interactiveBg || 0 ) - (x.interactiveBg || 0 ) ||
99 chroma (y.hex) - chroma (x.hex),
100 );
101 if (a. length ) return a[ 0 ].hex;
102 }
103 const c = (colors ?? [])
104 . map (String)
105 . filter (ok)
106 . sort (( x , y ) => chroma (y) - chroma (x));
107 return c[ 0 ];
108 }
109
110 // Derive brand roles from rich capture colorStats (areaBg / interactiveBg / textCount /
111 // maxArea) — by semantic FUNCTION, not luminance/chroma proxies. Returns null when stats
112 // are unusable, so the caller can fall back. canvas = the color painting the most real
113 // background area (the page ground, dark or light); ink = the dominant text color that
114 // actually contrasts with the canvas; accent via pickAccent.
115 export function brandRolesFromStats ( stats , colorsInOrder ) {
116 if ( ! Array. isArray (stats) || ! stats. length ) return null ;
117 const v = stats. filter (( s ) => / ^ # [0-9a-fA-F] {6}$ / . test (s?.hex || "" ));
118 if ( ! v. length ) return null ;
119 const canvas = [ ... v]. sort (
120 ( a , b ) =>
121 (b.areaBg || 0 ) - (a.areaBg || 0 ) ||
122 (b.maxArea || 0 ) - (a.maxArea || 0 ) ||
123 (b.bgCount || 0 ) - (a.bgCount || 0 ),
124 )[ 0 ]?.hex;
125 // pass the frequency-ordered palette (tokens.colors) so pickAccent can use palette
126 // PROMINENCE — colorStats counts alone are too sparse to rank rare accents.
127 const accent = pickAccent (v, colorsInOrder ?? v. map (( s ) => s.hex), [canvas]);
128 if ( ! canvas || ! accent) return null ;
129 const cl = lum (canvas) ?? 0 ;
130 const ink =
131 [ ... v]
132 . filter (( s ) => s.hex !== canvas && s.hex !== accent)
133 . sort (( a , b ) => (b.textCount || 0 ) - (a.textCount || 0 ))
134 . find (( s ) => Math. abs (( lum (s.hex) ?? 0 ) - cl) > 64 )?.hex ??
135 (cl > 128 ? "#000000" : "#FFFFFF" );
136 const accent2 =
137 pickAccent (v, colorsInOrder ?? v. map (( s ) => s.hex), [canvas, ink, accent]) ?? accent;
138 return { ink, canvas, accent, accent2 };
139 }
140
141 // Map a list of [key, value] colors to semantic roles. ink = a dark/ink-named
142 // color (else darkest); canvas = a paper/cream/white-named color (else lightest);
143 // accents = whatever's left, ranked by chroma (the loudest color is almost always
144 // the brand accent) — UA-default link colors AND semantic status colors (positive/
145 // negative/error…) excluded so neither a stray <a> color nor a status red ever wins.
146 // For an unkeyed brand list, pass synthetic keys — name matching simply no-ops and it
147 // falls back to luminance/chroma, which is what we want. NOTE: when capture colorStats
148 // exist, prefer brandRolesFromStats() — it picks by function, not these proxies.
149 export function semanticColors ( colors ) {
150 if ( ! colors. length ) return {};
151 const named = ( re ) => colors. find (([ k ]) => re. test (k));
152 const hexes = colors. filter (([, v ]) => lum (v) != null );
153 const byLum = [ ... hexes]. sort (( a , b ) => ( lum (a[ 1 ]) ?? 1e9 ) - ( lum (b[ 1 ]) ?? 1e9 ));
154 const pick = ( m , fallback ) => (m ? m[ 1 ] : fallback ? fallback[ 1 ] : undefined );
155 // "ink" must be a whole word-segment so "soft-pink"/"pink" don't match it.
156 const ink = pick (
157 named ( /(?: ^| [-_] )ink(?: [-_] |$ ) | black | charcoal |^ text(?:-dark) ?$| outline | noir/ i ),
158 byLum[ 0 ] ?? colors[ 0 ],
159 );
160 const canvas = pick (
161 named ( /cream | paper | canvas | white | bg | ground | surface | base | sand | parchment | off- ? white | bone/ i ),
162 byLum[byLum. length - 1 ] ?? colors[colors. length - 1 ],
163 );
164 const accents = colors
165 . filter (
166 ([ k , v ]) =>
167 v !== ink &&
168 v !== canvas &&
169 ! UA_DEFAULT_COLORS . has ( String (v). toUpperCase ()) &&
170 ! STATUS_ROLE_KEY . test (k), // a status red/green carries meaning by hue — never an accent
171 )
172 . sort (( a , b ) => chroma (b[ 1 ]) - chroma (a[ 1 ]))
173 . map (([, v ]) => v);
174 return { ink, canvas, accent: accents[ 0 ] ?? ink, accent2: accents[ 1 ] ?? accents[ 0 ] ?? ink };
175 }
176
177 // Collect role→fontFamily under the top-level `typography:` block; pick a display
178 // + body family from the usual role names. Returns quoted families (or null).
179 export function parseFonts ( md ) {
180 const roles = {};
181 let inBlock = false ;
182 for ( const line of md. split ( / \r ? \n / )) {
183 if ( / ^ typography: \s *$ / . test (line)) {
184 inBlock = true ;
185 continue ;
186 }
187 if ( ! inBlock) continue ;
188 if ( / ^ \S / . test (line)) break ;
189 const m = line. match ( / ^ \s + ( [\w-] + ): \s * \{ [ ^ }] * fontFamily: \s * "( [ ^ "] + )"/ );
190 if (m) roles[m[ 1 ]] = m[ 2 ];
191 }
192 const q = ( s ) => (s ? `"${ s }"` : null );
193 const body = roles.body ?? roles.subtitle ?? Object. values (roles)[ 0 ];
194 const display =
195 roles.display ??
196 roles.headline ??
197 roles[ "card-headline" ] ??
198 roles[ "section-headline" ] ??
199 roles[ "quote-display" ] ??
200 roles.h1 ??
201 roles.h2 ??
202 roles.title ??
203 roles.hero ??
204 body;
205 // the monospace / chrome family (code, tags, ticks, page numbers) — so the remix can
206 // route a captured brand mono (Berkeley Mono, JetBrains Mono…) onto this role instead
207 // of the reading body. null when the preset has no distinct mono role.
208 const mono =
209 roles.mono ??
210 roles[ "mono-tag" ] ??
211 roles[ "mono-chrome" ] ??
212 roles[ "mono-tick" ] ??
213 roles.code ??
214 roles.data ??
215 roles.pagenum ??
216 null ;
217 return { display: q (display), body: q (body), mono: q (mono) };
218 }