inkhouse 0.1.0-beta.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/README.md +201 -0
- package/bin/inkhouse.mjs +171 -0
- package/code.js +11802 -0
- package/manifest.json +30 -0
- package/package.json +45 -0
- package/scanner/blob-placement-regression.ts +132 -0
- package/scanner/class-collector.ts +69 -0
- package/scanner/cli.ts +336 -0
- package/scanner/component-scanner.ts +2876 -0
- package/scanner/css-patch-regression.ts +112 -0
- package/scanner/css-token-reader-regression.ts +92 -0
- package/scanner/css-token-reader.ts +477 -0
- package/scanner/font-style-resolver-regression.ts +32 -0
- package/scanner/index.ts +9 -0
- package/scanner/radial-gradient-regression.ts +53 -0
- package/scanner/style-map.ts +145 -0
- package/scanner/tailwind-parser.ts +644 -0
- package/scanner/transform-math-regression.ts +42 -0
- package/scanner/types.ts +298 -0
- package/src/blob-placement.ts +111 -0
- package/src/change-detection.ts +204 -0
- package/src/class-utils.ts +105 -0
- package/src/clip-path-decorative.ts +194 -0
- package/src/color-resolver.ts +98 -0
- package/src/colors.ts +196 -0
- package/src/component-defs.ts +54 -0
- package/src/component-gen.ts +561 -0
- package/src/component-lookup.ts +82 -0
- package/src/config.ts +115 -0
- package/src/design-system.ts +59 -0
- package/src/dev-server.ts +173 -0
- package/src/figma-globals.d.ts +3 -0
- package/src/font-style-resolver.ts +171 -0
- package/src/github.ts +1465 -0
- package/src/icon-builder.ts +607 -0
- package/src/image-cache.ts +22 -0
- package/src/inline-text.ts +271 -0
- package/src/layout-parser.ts +667 -0
- package/src/layout-utils.ts +155 -0
- package/src/main.ts +687 -0
- package/src/node-ir.ts +595 -0
- package/src/pack-provider.ts +148 -0
- package/src/packs.ts +126 -0
- package/src/radial-gradient.ts +84 -0
- package/src/render-context.ts +138 -0
- package/src/responsive-analyzer.ts +139 -0
- package/src/state-analyzer.ts +143 -0
- package/src/story-builder.ts +1706 -0
- package/src/story-layout.ts +38 -0
- package/src/tailwind.ts +2379 -0
- package/src/text-builder.ts +116 -0
- package/src/text-line.ts +42 -0
- package/src/token-source.ts +43 -0
- package/src/tokens.ts +717 -0
- package/src/transform-math.ts +44 -0
- package/src/ui-builder.ts +1996 -0
- package/src/utility-resolver.ts +125 -0
- package/src/variables.ts +1042 -0
- package/src/width-solver.ts +466 -0
- package/templates/patch-tokens-route.ts +165 -0
- package/templates/scan-components-route.ts +57 -0
- package/ui.html +1222 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { type RGB, parseColor } from './colors';
|
|
2
|
+
import { resolveTextColorValue, extractTextColorToken } from './color-resolver';
|
|
3
|
+
import { getLineHeightFromClasses, TAG_TYPOGRAPHY, createTextNode } from './text-builder';
|
|
4
|
+
import { tailwindClassesToStyle, type TailwindStyle } from './tailwind';
|
|
5
|
+
import { type NodeIR, type NodeIRElement } from './node-ir';
|
|
6
|
+
import type { RenderContext } from './render-context';
|
|
7
|
+
import { type NodeLayoutComputed } from './width-solver';
|
|
8
|
+
import { INLINE_TEXT_TAGS } from './text-builder';
|
|
9
|
+
import { TOKENS, getThemeFontFamily } from './tokens';
|
|
10
|
+
import { getFontStyleCandidatesForFamily, inferFontWeight } from './font-style-resolver';
|
|
11
|
+
|
|
12
|
+
export type InlineTextSegment = {
|
|
13
|
+
text: string;
|
|
14
|
+
fill?: RGB | null;
|
|
15
|
+
fontStyle?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const BLOCK_TEXT_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li']);
|
|
19
|
+
const FONT_WEIGHT_TO_STYLE: Record<string, string> = {
|
|
20
|
+
'font-thin': 'Thin',
|
|
21
|
+
'font-extralight': 'ExtraLight',
|
|
22
|
+
'font-light': 'Light',
|
|
23
|
+
'font-normal': 'Regular',
|
|
24
|
+
'font-medium': 'Medium',
|
|
25
|
+
'font-semibold': 'SemiBold',
|
|
26
|
+
'font-bold': 'Bold',
|
|
27
|
+
'font-extrabold': 'ExtraBold',
|
|
28
|
+
'font-black': 'Black',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function normalizeStyle(style: string | undefined): string {
|
|
32
|
+
return String(style || '').trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function styleHasItalic(style: string | undefined): boolean {
|
|
36
|
+
return /\bitalic\b/i.test(normalizeStyle(style));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function removeItalic(style: string | undefined): string {
|
|
40
|
+
const normalized = normalizeStyle(style);
|
|
41
|
+
if (!normalized) return '';
|
|
42
|
+
const stripped = normalized.replace(/\s*italic\b/gi, '').trim();
|
|
43
|
+
return stripped || 'Regular';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function applyItalicModifier(style: string | undefined, classes: string[]): string | undefined {
|
|
47
|
+
let next = style;
|
|
48
|
+
if (classes.includes('italic')) {
|
|
49
|
+
if (!styleHasItalic(next)) {
|
|
50
|
+
const base = normalizeStyle(next) || 'Regular';
|
|
51
|
+
next = base === 'Italic' ? base : base + ' Italic';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (classes.includes('not-italic')) {
|
|
55
|
+
next = removeItalic(next);
|
|
56
|
+
}
|
|
57
|
+
return next;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveFontStyleFromClasses(classes: string[], fallback?: string): string | undefined {
|
|
61
|
+
let found: string | undefined;
|
|
62
|
+
for (const cls of classes) {
|
|
63
|
+
const mapped = FONT_WEIGHT_TO_STYLE[cls];
|
|
64
|
+
if (mapped) found = mapped;
|
|
65
|
+
}
|
|
66
|
+
return applyItalicModifier(found || fallback, classes);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ensureAtLeastBold(style: string | undefined): string {
|
|
70
|
+
const current = normalizeStyle(style) || 'Regular';
|
|
71
|
+
if (inferFontWeight(current) >= 700) return current;
|
|
72
|
+
return styleHasItalic(current) ? 'Bold Italic' : 'Bold';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isInlineTextNode(node: NodeIR): boolean {
|
|
76
|
+
if (node.kind === 'text') return true;
|
|
77
|
+
if (node.kind === 'fragment') return node.children.every(isInlineTextNode);
|
|
78
|
+
if (node.kind === 'element' && INLINE_TEXT_TAGS.has(node.tagLower)) {
|
|
79
|
+
return (node.children || []).every(isInlineTextNode);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeInlineSegments(segments: InlineTextSegment[]): InlineTextSegment[] {
|
|
85
|
+
const out: InlineTextSegment[] = [];
|
|
86
|
+
for (const seg of segments) {
|
|
87
|
+
if (!seg.text) continue;
|
|
88
|
+
if (out.length > 0) {
|
|
89
|
+
const last = out[out.length - 1];
|
|
90
|
+
const lastEndsWithSpace = /\\s$/.test(last.text);
|
|
91
|
+
const nextStartsWithSpace = /^\\s/.test(seg.text);
|
|
92
|
+
if (!lastEndsWithSpace && !nextStartsWithSpace) {
|
|
93
|
+
last.text += ' ';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
out.push(seg);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function collectInlineSegments(
|
|
102
|
+
node: NodeIR,
|
|
103
|
+
parentStyle: { fill?: RGB | null; fontStyle?: string },
|
|
104
|
+
colorGroup: Record<string, string>,
|
|
105
|
+
theme: string
|
|
106
|
+
): InlineTextSegment[] {
|
|
107
|
+
if (node.kind === 'text') {
|
|
108
|
+
return [{ text: node.text, fill: parentStyle.fill, fontStyle: parentStyle.fontStyle }];
|
|
109
|
+
}
|
|
110
|
+
if (node.kind === 'fragment') {
|
|
111
|
+
const parts: InlineTextSegment[] = [];
|
|
112
|
+
for (const child of node.children) {
|
|
113
|
+
parts.push.apply(parts, collectInlineSegments(child, parentStyle, colorGroup, theme));
|
|
114
|
+
}
|
|
115
|
+
return parts;
|
|
116
|
+
}
|
|
117
|
+
if (node.kind === 'element' && INLINE_TEXT_TAGS.has(node.tagLower)) {
|
|
118
|
+
const style = tailwindClassesToStyle(node.classes, 'default', colorGroup);
|
|
119
|
+
const token = style.textToken || extractTextColorToken(node.classes) || null;
|
|
120
|
+
const resolved = resolveTextColorValue(style.text, token, colorGroup, theme);
|
|
121
|
+
const fill = resolved ? parseColor(resolved) : parentStyle.fill;
|
|
122
|
+
let fontStyle = resolveFontStyleFromClasses(node.classes, parentStyle.fontStyle);
|
|
123
|
+
if (node.tagLower === 'strong' || node.tagLower === 'b') {
|
|
124
|
+
fontStyle = ensureAtLeastBold(fontStyle);
|
|
125
|
+
}
|
|
126
|
+
const nextStyle = { fill: fill, fontStyle: fontStyle };
|
|
127
|
+
const parts: InlineTextSegment[] = [];
|
|
128
|
+
for (const child of node.children || []) {
|
|
129
|
+
parts.push.apply(parts, collectInlineSegments(child, nextStyle, colorGroup, theme));
|
|
130
|
+
}
|
|
131
|
+
return parts;
|
|
132
|
+
}
|
|
133
|
+
if (node.kind === 'element') {
|
|
134
|
+
const parts: InlineTextSegment[] = [];
|
|
135
|
+
for (const child of node.children || []) {
|
|
136
|
+
parts.push.apply(parts, collectInlineSegments(child, parentStyle, colorGroup, theme));
|
|
137
|
+
}
|
|
138
|
+
return parts;
|
|
139
|
+
}
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createInlineTextNode(
|
|
144
|
+
node: NodeIRElement,
|
|
145
|
+
textStyle: TailwindStyle,
|
|
146
|
+
context: RenderContext,
|
|
147
|
+
colorGroup: Record<string, string>,
|
|
148
|
+
theme: string,
|
|
149
|
+
layoutComputed: NodeLayoutComputed
|
|
150
|
+
): TextNode | null {
|
|
151
|
+
const baseFillValue = resolveTextColorValue(
|
|
152
|
+
textStyle.text || context.textColor,
|
|
153
|
+
textStyle.textToken || context.textColorToken,
|
|
154
|
+
colorGroup,
|
|
155
|
+
theme
|
|
156
|
+
);
|
|
157
|
+
const baseFill = baseFillValue ? parseColor(baseFillValue) : undefined;
|
|
158
|
+
const baseFontStyle = resolveFontStyleFromClasses(
|
|
159
|
+
node.classes,
|
|
160
|
+
context.textFontStyle || (context.textBold ? 'Bold' : undefined)
|
|
161
|
+
);
|
|
162
|
+
const segments = normalizeInlineSegments(
|
|
163
|
+
collectInlineSegments(node, { fill: baseFill, fontStyle: baseFontStyle }, colorGroup, theme)
|
|
164
|
+
);
|
|
165
|
+
if (segments.length === 0) return null;
|
|
166
|
+
const textValue = segments.map(seg => seg.text).join('');
|
|
167
|
+
|
|
168
|
+
const typo = TAG_TYPOGRAPHY[node.tagLower] || { fontSize: 14 };
|
|
169
|
+
const textOpts: any = {
|
|
170
|
+
fontSize: typo.fontSize,
|
|
171
|
+
lineHeight: typo.lineHeight,
|
|
172
|
+
};
|
|
173
|
+
if (baseFontStyle) textOpts.fontStyle = baseFontStyle;
|
|
174
|
+
else if (typo.bold) textOpts.bold = true;
|
|
175
|
+
if (baseFill) textOpts.fill = baseFill;
|
|
176
|
+
if (node.classes.includes('text-center')) {
|
|
177
|
+
textOpts.textAlignHorizontal = 'CENTER';
|
|
178
|
+
} else if (node.classes.includes('text-right')) {
|
|
179
|
+
textOpts.textAlignHorizontal = 'RIGHT';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const sizeClass = node.classes.find(c => c.match(/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)/));
|
|
183
|
+
if (sizeClass) {
|
|
184
|
+
const sizeMap: Record<string, number> = {
|
|
185
|
+
'text-xs': 12, 'text-sm': 14, 'text-base': 16,
|
|
186
|
+
'text-lg': 18, 'text-xl': 20, 'text-2xl': 24,
|
|
187
|
+
'text-3xl': 30, 'text-4xl': 36, 'text-5xl': 48
|
|
188
|
+
};
|
|
189
|
+
if (sizeMap[sizeClass]) textOpts.fontSize = sizeMap[sizeClass];
|
|
190
|
+
}
|
|
191
|
+
const lineHeight = getLineHeightFromClasses(node.classes, textOpts.fontSize);
|
|
192
|
+
if (lineHeight) {
|
|
193
|
+
textOpts.lineHeight = lineHeight;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
textOpts.theme = theme;
|
|
197
|
+
const textNode = createTextNode(textValue, textOpts);
|
|
198
|
+
|
|
199
|
+
const baseFontName = textNode.fontName && typeof textNode.fontName === 'object' ? textNode.fontName : null;
|
|
200
|
+
const fontFamily = (baseFontName && 'family' in baseFontName ? (baseFontName as any).family : null)
|
|
201
|
+
|| getThemeFontFamily(TOKENS, theme)
|
|
202
|
+
|| 'Inter';
|
|
203
|
+
const baseStyle = baseFontName && 'style' in baseFontName ? (baseFontName as any).style : (baseFontStyle || 'Regular');
|
|
204
|
+
let cursor = 0;
|
|
205
|
+
for (const seg of segments) {
|
|
206
|
+
const start = cursor;
|
|
207
|
+
const end = cursor + seg.text.length;
|
|
208
|
+
cursor = end;
|
|
209
|
+
if (seg.fill) {
|
|
210
|
+
try {
|
|
211
|
+
textNode.setRangeFills(start, end, [{
|
|
212
|
+
type: 'SOLID',
|
|
213
|
+
color: { r: seg.fill.r, g: seg.fill.g, b: seg.fill.b },
|
|
214
|
+
opacity: seg.fill.a == null ? 1 : seg.fill.a,
|
|
215
|
+
}]);
|
|
216
|
+
} catch (_err) {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (seg.fontStyle && seg.fontStyle !== baseStyle) {
|
|
221
|
+
const styleCandidates = getFontStyleCandidatesForFamily(fontFamily, seg.fontStyle);
|
|
222
|
+
for (const candidate of styleCandidates) {
|
|
223
|
+
try {
|
|
224
|
+
textNode.setRangeFontName(start, end, { family: fontFamily, style: candidate });
|
|
225
|
+
break;
|
|
226
|
+
} catch (_err) {
|
|
227
|
+
// try next candidate
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let constrainWidth = context.maxWidth;
|
|
234
|
+
if (layoutComputed.explicitWidth != null) {
|
|
235
|
+
constrainWidth = layoutComputed.explicitWidth;
|
|
236
|
+
} else if (layoutComputed.maxWidth != null) {
|
|
237
|
+
constrainWidth = context.maxWidth != null
|
|
238
|
+
? Math.min(layoutComputed.maxWidth, context.maxWidth)
|
|
239
|
+
: layoutComputed.maxWidth;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const hasInlineDisplayClass =
|
|
243
|
+
node.classes.includes('inline')
|
|
244
|
+
|| node.classes.includes('inline-block')
|
|
245
|
+
|| node.classes.includes('inline-flex')
|
|
246
|
+
|| node.classes.includes('inline-grid')
|
|
247
|
+
|| node.classes.includes('contents');
|
|
248
|
+
const shouldInheritContainerWidth =
|
|
249
|
+
!!constrainWidth
|
|
250
|
+
&& !hasInlineDisplayClass
|
|
251
|
+
&& !INLINE_TEXT_TAGS.has(node.tagLower);
|
|
252
|
+
|
|
253
|
+
const shouldConstrain =
|
|
254
|
+
BLOCK_TEXT_TAGS.has(node.tagLower)
|
|
255
|
+
|| layoutComputed.explicitWidth != null
|
|
256
|
+
|| layoutComputed.maxWidth != null
|
|
257
|
+
|| node.classes.includes('block')
|
|
258
|
+
|| node.classes.includes('w-full')
|
|
259
|
+
|| shouldInheritContainerWidth;
|
|
260
|
+
|
|
261
|
+
if (shouldConstrain && constrainWidth) {
|
|
262
|
+
try {
|
|
263
|
+
textNode.textAutoResize = 'HEIGHT';
|
|
264
|
+
textNode.resize(constrainWidth, textNode.height);
|
|
265
|
+
} catch (_err) {
|
|
266
|
+
// ignore resize errors
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return textNode;
|
|
271
|
+
}
|