docgen-utils 1.0.11 → 1.0.13
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/dist/bundle.js +42918 -6708
- package/dist/bundle.min.js +289 -109
- package/dist/cli.js +26450 -1266
- package/dist/packages/cli/commands/export-docs.d.ts.map +1 -1
- package/dist/packages/cli/commands/export-docs.js +131 -2
- package/dist/packages/cli/commands/export-docs.js.map +1 -1
- package/dist/packages/cli/commands/export-slides.d.ts.map +1 -1
- package/dist/packages/cli/commands/export-slides.js +25 -1
- package/dist/packages/cli/commands/export-slides.js.map +1 -1
- package/dist/packages/docs/common.d.ts +10 -0
- package/dist/packages/docs/common.d.ts.map +1 -1
- package/dist/packages/docs/common.js.map +1 -1
- package/dist/packages/docs/convert.d.ts.map +1 -1
- package/dist/packages/docs/convert.js +246 -218
- package/dist/packages/docs/convert.js.map +1 -1
- package/dist/packages/docs/create-document.d.ts.map +1 -1
- package/dist/packages/docs/create-document.js +43 -3
- package/dist/packages/docs/create-document.js.map +1 -1
- package/dist/packages/docs/export.d.ts +9 -8
- package/dist/packages/docs/export.d.ts.map +1 -1
- package/dist/packages/docs/export.js +23 -36
- package/dist/packages/docs/export.js.map +1 -1
- package/dist/packages/docs/import-docx.d.ts.map +1 -1
- package/dist/packages/docs/import-docx.js +397 -7
- package/dist/packages/docs/import-docx.js.map +1 -1
- package/dist/packages/docs/parse-colors.d.ts +37 -0
- package/dist/packages/docs/parse-colors.d.ts.map +1 -0
- package/dist/packages/docs/parse-colors.js +507 -0
- package/dist/packages/docs/parse-colors.js.map +1 -0
- package/dist/packages/docs/parse-css.d.ts +98 -0
- package/dist/packages/docs/parse-css.d.ts.map +1 -0
- package/dist/packages/docs/parse-css.js +1592 -0
- package/dist/packages/docs/parse-css.js.map +1 -0
- package/dist/packages/docs/parse-helpers.d.ts +45 -0
- package/dist/packages/docs/parse-helpers.d.ts.map +1 -0
- package/dist/packages/docs/parse-helpers.js +214 -0
- package/dist/packages/docs/parse-helpers.js.map +1 -0
- package/dist/packages/docs/parse-inline.d.ts +41 -0
- package/dist/packages/docs/parse-inline.d.ts.map +1 -0
- package/dist/packages/docs/parse-inline.js +473 -0
- package/dist/packages/docs/parse-inline.js.map +1 -0
- package/dist/packages/docs/parse-layout.d.ts +57 -0
- package/dist/packages/docs/parse-layout.d.ts.map +1 -0
- package/dist/packages/docs/parse-layout.js +295 -0
- package/dist/packages/docs/parse-layout.js.map +1 -0
- package/dist/packages/docs/parse-special.d.ts +51 -0
- package/dist/packages/docs/parse-special.d.ts.map +1 -0
- package/dist/packages/docs/parse-special.js +251 -0
- package/dist/packages/docs/parse-special.js.map +1 -0
- package/dist/packages/docs/parse-units.d.ts +68 -0
- package/dist/packages/docs/parse-units.d.ts.map +1 -0
- package/dist/packages/docs/parse-units.js +275 -0
- package/dist/packages/docs/parse-units.js.map +1 -0
- package/dist/packages/docs/parse.d.ts.map +1 -1
- package/dist/packages/docs/parse.js +957 -2800
- package/dist/packages/docs/parse.js.map +1 -1
- package/dist/packages/slides/common.d.ts +7 -0
- package/dist/packages/slides/common.d.ts.map +1 -1
- package/dist/packages/slides/convert.d.ts.map +1 -1
- package/dist/packages/slides/convert.js +92 -7
- package/dist/packages/slides/convert.js.map +1 -1
- package/dist/packages/slides/fonts.d.ts +41 -0
- package/dist/packages/slides/fonts.d.ts.map +1 -0
- package/dist/packages/slides/fonts.js +209 -0
- package/dist/packages/slides/fonts.js.map +1 -0
- package/dist/packages/slides/import-pptx.d.ts.map +1 -1
- package/dist/packages/slides/import-pptx.js +583 -120
- package/dist/packages/slides/import-pptx.js.map +1 -1
- package/dist/packages/slides/parse.d.ts.map +1 -1
- package/dist/packages/slides/parse.js +724 -91
- package/dist/packages/slides/parse.js.map +1 -1
- package/dist/packages/slides/transform.d.ts +6 -6
- package/dist/packages/slides/transform.d.ts.map +1 -1
- package/dist/packages/slides/transform.js +25 -51
- package/dist/packages/slides/transform.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,1592 @@
|
|
|
1
|
+
import { extractFirstGradientColor, extractHexColor, extractPrimaryFont, parseGradient } from "./parse-colors";
|
|
2
|
+
/**
|
|
3
|
+
* Remove @media blocks from CSS text by tracking brace nesting.
|
|
4
|
+
* This properly handles nested braces within media queries.
|
|
5
|
+
*/
|
|
6
|
+
export function removeMediaQueries(cssText) {
|
|
7
|
+
let result = "";
|
|
8
|
+
let i = 0;
|
|
9
|
+
while (i < cssText.length) {
|
|
10
|
+
// Check for @media at current position
|
|
11
|
+
if (cssText.substring(i, i + 6).toLowerCase() === "@media") {
|
|
12
|
+
// Find the opening brace
|
|
13
|
+
let braceStart = cssText.indexOf("{", i);
|
|
14
|
+
if (braceStart === -1) {
|
|
15
|
+
// Malformed CSS, include rest as-is
|
|
16
|
+
result += cssText.substring(i);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
// Track brace depth to find matching closing brace
|
|
20
|
+
let depth = 1;
|
|
21
|
+
let j = braceStart + 1;
|
|
22
|
+
while (j < cssText.length && depth > 0) {
|
|
23
|
+
if (cssText[j] === "{") {
|
|
24
|
+
depth++;
|
|
25
|
+
}
|
|
26
|
+
else if (cssText[j] === "}") {
|
|
27
|
+
depth--;
|
|
28
|
+
}
|
|
29
|
+
j++;
|
|
30
|
+
}
|
|
31
|
+
// Skip the entire @media block (from @media to matching })
|
|
32
|
+
i = j;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
result += cssText[i];
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
export function parseCssContext(doc) {
|
|
42
|
+
const variables = new Map();
|
|
43
|
+
const classColors = new Map();
|
|
44
|
+
const calloutStyles = new Map();
|
|
45
|
+
const classStyles = new Map();
|
|
46
|
+
const elementStyles = new Map();
|
|
47
|
+
const nestedStyles = new Map();
|
|
48
|
+
// Helper to resolve CSS variable or return value as-is
|
|
49
|
+
// Handles multiple var() references in a single value (e.g., "var(--a) var(--b)")
|
|
50
|
+
const resolveValue = (value) => {
|
|
51
|
+
// Replace all var() references with their resolved values
|
|
52
|
+
// Supports var(--name) and var(--name, fallback) syntax
|
|
53
|
+
return value.replace(/var\s*\(\s*(--[a-zA-Z0-9-]+)\s*(?:,\s*([^)]+))?\s*\)/g, (match, varName, fallback) => {
|
|
54
|
+
const resolved = variables.get(varName);
|
|
55
|
+
if (resolved)
|
|
56
|
+
return resolved;
|
|
57
|
+
if (fallback)
|
|
58
|
+
return fallback.trim();
|
|
59
|
+
return match; // Keep original if not found
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
// Find all style elements
|
|
63
|
+
const styleElements = doc.querySelectorAll("style");
|
|
64
|
+
for (const styleEl of styleElements) {
|
|
65
|
+
let cssText = styleEl.textContent || "";
|
|
66
|
+
// Remove media queries to avoid mobile-specific styles overriding desktop defaults
|
|
67
|
+
// Use a function-based approach to correctly handle nested braces
|
|
68
|
+
cssText = removeMediaQueries(cssText);
|
|
69
|
+
// Extract CSS variables from :root { --name: value; }
|
|
70
|
+
// Use matchAll to handle multiple :root blocks (e.g., from concatenated stylesheets)
|
|
71
|
+
const rootMatches = cssText.matchAll(/:root\s*\{([^}]+)\}/g);
|
|
72
|
+
for (const rootMatch of rootMatches) {
|
|
73
|
+
const rootContent = rootMatch[1];
|
|
74
|
+
const varMatches = rootContent.matchAll(/--([a-zA-Z0-9-]+)\s*:\s*([^;]+)/g);
|
|
75
|
+
for (const match of varMatches) {
|
|
76
|
+
const varName = `--${match[1]}`;
|
|
77
|
+
const varValue = match[2].trim();
|
|
78
|
+
variables.set(varName, varValue);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Also extract CSS variables from theme classes (e.g., .theme-professional)
|
|
82
|
+
// These are commonly used to scope CSS variables to body elements
|
|
83
|
+
const themeClassMatches = cssText.matchAll(/\.theme-[a-zA-Z0-9_-]+\s*\{([^}]+)\}/g);
|
|
84
|
+
for (const themeMatch of themeClassMatches) {
|
|
85
|
+
const themeContent = themeMatch[1];
|
|
86
|
+
const varMatches = themeContent.matchAll(/--([a-zA-Z0-9-]+)\s*:\s*([^;]+)/g);
|
|
87
|
+
for (const match of varMatches) {
|
|
88
|
+
const varName = `--${match[1]}`;
|
|
89
|
+
const varValue = match[2].trim();
|
|
90
|
+
// Only set if not already defined (prefer :root values)
|
|
91
|
+
if (!variables.has(varName)) {
|
|
92
|
+
variables.set(varName, varValue);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Extract class color rules: .classname { color: value; }
|
|
97
|
+
// Matches single or comma-separated class selectors: .class1, .class2 { ... }
|
|
98
|
+
// Uses a broad regex to capture the full selector list and rule body.
|
|
99
|
+
// Handles pseudo-class selectors by filtering them in the processing loop.
|
|
100
|
+
const classRuleMatches = cssText.matchAll(/(?:^|[;\n}])\s*((?:\.[\w-]+(?:[:\w()\-]*)?(?:\s*,\s*)?)+)\s*(\{[^}]+\})/gm);
|
|
101
|
+
for (const match of classRuleMatches) {
|
|
102
|
+
const fullMatch = match[0];
|
|
103
|
+
const selectorList = match[1];
|
|
104
|
+
const ruleBlock = match[2];
|
|
105
|
+
const ruleContent = ruleBlock.slice(1, -1); // Remove { }
|
|
106
|
+
// Split comma-separated selectors and process each one
|
|
107
|
+
const selectors = selectorList.split(/\s*,\s*/).map(s => s.trim()).filter(s => s.startsWith('.'));
|
|
108
|
+
const classNames = [];
|
|
109
|
+
for (const selector of selectors) {
|
|
110
|
+
// Skip pseudo-class selectors like .foo:hover, .foo:focus, .foo:active, .foo:visited
|
|
111
|
+
if (selector.match(/:(?:hover|focus|active|visited|focus-within|focus-visible)\b/)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Skip pseudo-element selectors like .foo::before, .foo::after
|
|
115
|
+
if (selector.match(/::(?:before|after|first-line|first-letter|placeholder|selection)\b/)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Extract class name(s) - handles compound selectors like .classA.classB
|
|
119
|
+
// For compound selectors, we extract ALL class parts
|
|
120
|
+
const allClassParts = [...selector.matchAll(/\.([a-zA-Z0-9_-]+)/g)].map(m => m[1]);
|
|
121
|
+
if (allClassParts.length === 0)
|
|
122
|
+
continue;
|
|
123
|
+
if (allClassParts.length === 1) {
|
|
124
|
+
// Simple selector: .classA
|
|
125
|
+
classNames.push(allClassParts[0]);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Compound selector: .classA.classB - store under a compound key
|
|
129
|
+
// Sort to ensure consistent key regardless of order in CSS
|
|
130
|
+
const compoundKey = allClassParts.sort().join('.');
|
|
131
|
+
classNames.push(compoundKey);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (classNames.length === 0)
|
|
135
|
+
continue;
|
|
136
|
+
// Skip if this looks like a nested selector (has another selector before it)
|
|
137
|
+
// Check if the match is preceded by another class, element, or ID selector on the same line
|
|
138
|
+
// This catches ".parent .child", "figure.parent .child", "#id .child", "div .child" etc.
|
|
139
|
+
const precedingText = cssText.substring(0, match.index).split('\n').pop() || '';
|
|
140
|
+
if (precedingText.match(/(?:\.[a-zA-Z0-9_-]+|#[a-zA-Z0-9_-]+|[a-zA-Z][a-zA-Z0-9]*)\s*$/)) {
|
|
141
|
+
// This is a nested selector like ".parent .child { ... }" - skip it here
|
|
142
|
+
// Nested selectors will be handled by storing parent-child relationships
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// Extract ALL style properties for this class (generalized approach)
|
|
146
|
+
const style = {};
|
|
147
|
+
// Text color
|
|
148
|
+
const colorMatch = ruleContent.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
|
|
149
|
+
if (colorMatch) {
|
|
150
|
+
const colorValue = resolveValue(colorMatch[1].trim());
|
|
151
|
+
for (const className of classNames) {
|
|
152
|
+
classColors.set(className, colorValue);
|
|
153
|
+
}
|
|
154
|
+
style.color = colorValue;
|
|
155
|
+
}
|
|
156
|
+
// Background color
|
|
157
|
+
const bgMatch = ruleContent.match(/background(?:-(?:color|image))?\s*:\s*([^;]+)/i);
|
|
158
|
+
if (bgMatch) {
|
|
159
|
+
const bgValue = resolveValue(bgMatch[1].trim());
|
|
160
|
+
// For gradient backgrounds, extract the first color as fallback for DOCX
|
|
161
|
+
// Also store the full gradient for containers that support gradient rendering
|
|
162
|
+
const gradientColor = extractFirstGradientColor(bgValue);
|
|
163
|
+
if (gradientColor) {
|
|
164
|
+
style.backgroundColor = gradientColor;
|
|
165
|
+
// Parse full gradient for container backgrounds (not text gradients)
|
|
166
|
+
const gradient = parseGradient(bgValue);
|
|
167
|
+
if (gradient) {
|
|
168
|
+
style.backgroundGradient = gradient;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
style.backgroundColor = bgValue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Gradient text detection: when background-clip: text is used with a gradient background,
|
|
176
|
+
// extract the first color from the gradient as fallback and also store full gradient
|
|
177
|
+
const hasBackgroundClipText = ruleContent.match(/(?:-webkit-)?background-clip\s*:\s*text/i);
|
|
178
|
+
if (hasBackgroundClipText && bgMatch) {
|
|
179
|
+
const bgValue = resolveValue(bgMatch[1].trim());
|
|
180
|
+
// Parse full gradient
|
|
181
|
+
const gradient = parseGradient(bgValue);
|
|
182
|
+
if (gradient) {
|
|
183
|
+
style.gradient = gradient;
|
|
184
|
+
// Clear backgroundColor since the gradient is for text fill, NOT paragraph background
|
|
185
|
+
// Without this, elements get both a solid purple background shading AND gradient text
|
|
186
|
+
style.backgroundColor = undefined;
|
|
187
|
+
style.backgroundGradient = undefined;
|
|
188
|
+
// Also set fallback color from first stop
|
|
189
|
+
if (!style.color) {
|
|
190
|
+
style.color = gradient.stops[0]?.color;
|
|
191
|
+
if (style.color) {
|
|
192
|
+
for (const className of classNames) {
|
|
193
|
+
classColors.set(className, style.color);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// Fallback: extract first color if gradient parsing fails
|
|
200
|
+
const gradientColor = extractFirstGradientColor(bgValue);
|
|
201
|
+
if (gradientColor && !style.color) {
|
|
202
|
+
style.color = gradientColor;
|
|
203
|
+
for (const className of classNames) {
|
|
204
|
+
classColors.set(className, gradientColor);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Clear backgroundColor for text gradient
|
|
208
|
+
style.backgroundColor = undefined;
|
|
209
|
+
style.backgroundGradient = undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Border color
|
|
213
|
+
const borderColorMatch = ruleContent.match(/border-color\s*:\s*([^;]+)/i);
|
|
214
|
+
if (borderColorMatch) {
|
|
215
|
+
style.borderColor = resolveValue(borderColorMatch[1].trim());
|
|
216
|
+
}
|
|
217
|
+
// Border shorthand (e.g., "1px solid #e5e7eb")
|
|
218
|
+
const borderMatch = ruleContent.match(/(?:^|;)\s*border\s*:\s*([^;]+)/i);
|
|
219
|
+
if (borderMatch) {
|
|
220
|
+
style.border = resolveValue(borderMatch[1].trim());
|
|
221
|
+
}
|
|
222
|
+
// Border-left (used by callouts: "4px solid #2563eb")
|
|
223
|
+
const borderLeftMatch = ruleContent.match(/border-left\s*:\s*([^;]+)/i);
|
|
224
|
+
if (borderLeftMatch) {
|
|
225
|
+
const borderLeftValue = resolveValue(borderLeftMatch[1].trim());
|
|
226
|
+
// If it contains a color, extract it as borderColor
|
|
227
|
+
const colorInBorder = borderLeftValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
228
|
+
if (colorInBorder && !style.borderColor) {
|
|
229
|
+
style.borderColor = `#${colorInBorder[1]}`;
|
|
230
|
+
}
|
|
231
|
+
// Store the full border-left value (but NOT in style.border, which is for
|
|
232
|
+
// the CSS `border:` shorthand. Previously this was set as a fallback which
|
|
233
|
+
// could cause border-left-only elements to be misclassified as full-border.)
|
|
234
|
+
style.borderLeft = borderLeftValue;
|
|
235
|
+
}
|
|
236
|
+
// Border-right (used by sidebar dividers, etc.)
|
|
237
|
+
const borderRightMatch = ruleContent.match(/border-right\s*:\s*([^;]+)/i);
|
|
238
|
+
if (borderRightMatch) {
|
|
239
|
+
style.borderRight = resolveValue(borderRightMatch[1].trim());
|
|
240
|
+
}
|
|
241
|
+
// Border-bottom (used by title blocks with divider lines)
|
|
242
|
+
const borderBottomMatch = ruleContent.match(/border-bottom\s*:\s*([^;]+)/i);
|
|
243
|
+
if (borderBottomMatch) {
|
|
244
|
+
style.borderBottom = resolveValue(borderBottomMatch[1].trim());
|
|
245
|
+
}
|
|
246
|
+
// Border-top (used by footer sections with top divider lines)
|
|
247
|
+
const borderTopMatch = ruleContent.match(/border-top\s*:\s*([^;]+)/i);
|
|
248
|
+
if (borderTopMatch) {
|
|
249
|
+
style.borderTop = resolveValue(borderTopMatch[1].trim());
|
|
250
|
+
}
|
|
251
|
+
// Display property (grid, flex, block, etc.)
|
|
252
|
+
const displayMatch = ruleContent.match(/display\s*:\s*([^;]+)/i);
|
|
253
|
+
if (displayMatch) {
|
|
254
|
+
style.display = resolveValue(displayMatch[1].trim());
|
|
255
|
+
}
|
|
256
|
+
// Flex property (for flex item sizing like "flex: 1")
|
|
257
|
+
const flexMatch = ruleContent.match(/(?:^|;)\s*flex\s*:\s*([^;]+)/i);
|
|
258
|
+
if (flexMatch) {
|
|
259
|
+
style.flex = resolveValue(flexMatch[1].trim());
|
|
260
|
+
}
|
|
261
|
+
// Flex-direction property (for horizontal vs vertical flex containers)
|
|
262
|
+
const flexDirectionMatch = ruleContent.match(/flex-direction\s*:\s*([^;]+)/i);
|
|
263
|
+
if (flexDirectionMatch) {
|
|
264
|
+
style.flexDirection = resolveValue(flexDirectionMatch[1].trim());
|
|
265
|
+
}
|
|
266
|
+
// Flex-wrap property (for wrapping behavior)
|
|
267
|
+
const flexWrapMatch = ruleContent.match(/flex-wrap\s*:\s*([^;]+)/i);
|
|
268
|
+
if (flexWrapMatch) {
|
|
269
|
+
style.flexWrap = resolveValue(flexWrapMatch[1].trim());
|
|
270
|
+
}
|
|
271
|
+
// Justify-content property (for flex alignment: center, space-between, etc.)
|
|
272
|
+
const justifyContentMatch = ruleContent.match(/justify-content\s*:\s*([^;]+)/i);
|
|
273
|
+
if (justifyContentMatch) {
|
|
274
|
+
style.justifyContent = resolveValue(justifyContentMatch[1].trim());
|
|
275
|
+
}
|
|
276
|
+
// Gap property (for flex/grid spacing)
|
|
277
|
+
const gapMatch = ruleContent.match(/(?:^|;)\s*gap\s*:\s*([^;]+)/i);
|
|
278
|
+
if (gapMatch) {
|
|
279
|
+
style.gap = resolveValue(gapMatch[1].trim());
|
|
280
|
+
}
|
|
281
|
+
// Grid template columns (for two-column layout detection)
|
|
282
|
+
const gridColsMatch = ruleContent.match(/grid-template-columns\s*:\s*([^;]+)/i);
|
|
283
|
+
if (gridColsMatch) {
|
|
284
|
+
style.gridTemplateColumns = resolveValue(gridColsMatch[1].trim());
|
|
285
|
+
}
|
|
286
|
+
// Text alignment
|
|
287
|
+
const textAlignMatch = ruleContent.match(/text-align\s*:\s*([^;]+)/i);
|
|
288
|
+
if (textAlignMatch) {
|
|
289
|
+
style.textAlign = resolveValue(textAlignMatch[1].trim());
|
|
290
|
+
}
|
|
291
|
+
// Font size
|
|
292
|
+
const fontSizeMatch = ruleContent.match(/font-size\s*:\s*([^;]+)/i);
|
|
293
|
+
if (fontSizeMatch) {
|
|
294
|
+
style.fontSize = resolveValue(fontSizeMatch[1].trim());
|
|
295
|
+
}
|
|
296
|
+
// Font weight
|
|
297
|
+
const fontWeightMatch = ruleContent.match(/font-weight\s*:\s*([^;]+)/i);
|
|
298
|
+
if (fontWeightMatch) {
|
|
299
|
+
style.fontWeight = resolveValue(fontWeightMatch[1].trim());
|
|
300
|
+
}
|
|
301
|
+
// Padding shorthand
|
|
302
|
+
const paddingMatch = ruleContent.match(/(?:^|;)\s*padding\s*:\s*([^;]+)/i);
|
|
303
|
+
if (paddingMatch) {
|
|
304
|
+
style.padding = resolveValue(paddingMatch[1].trim());
|
|
305
|
+
}
|
|
306
|
+
// Individual padding properties (padding-left, padding-right, etc.)
|
|
307
|
+
const paddingLeftMatch = ruleContent.match(/padding-left\s*:\s*([^;]+)/i);
|
|
308
|
+
if (paddingLeftMatch) {
|
|
309
|
+
style.paddingLeft = resolveValue(paddingLeftMatch[1].trim());
|
|
310
|
+
}
|
|
311
|
+
const paddingRightMatch = ruleContent.match(/padding-right\s*:\s*([^;]+)/i);
|
|
312
|
+
if (paddingRightMatch) {
|
|
313
|
+
style.paddingRight = resolveValue(paddingRightMatch[1].trim());
|
|
314
|
+
}
|
|
315
|
+
const paddingTopMatch = ruleContent.match(/padding-top\s*:\s*([^;]+)/i);
|
|
316
|
+
if (paddingTopMatch) {
|
|
317
|
+
style.paddingTop = resolveValue(paddingTopMatch[1].trim());
|
|
318
|
+
}
|
|
319
|
+
const paddingBottomMatch = ruleContent.match(/padding-bottom\s*:\s*([^;]+)/i);
|
|
320
|
+
if (paddingBottomMatch) {
|
|
321
|
+
style.paddingBottom = resolveValue(paddingBottomMatch[1].trim());
|
|
322
|
+
}
|
|
323
|
+
// Font style (italic, normal)
|
|
324
|
+
const fontStyleMatch = ruleContent.match(/font-style\s*:\s*([^;]+)/i);
|
|
325
|
+
if (fontStyleMatch) {
|
|
326
|
+
style.fontStyle = resolveValue(fontStyleMatch[1].trim());
|
|
327
|
+
}
|
|
328
|
+
// Font family (extract primary font from font stack)
|
|
329
|
+
const fontFamilyMatch = ruleContent.match(/font-family\s*:\s*([^;]+)/i);
|
|
330
|
+
if (fontFamilyMatch) {
|
|
331
|
+
const resolved = resolveValue(fontFamilyMatch[1].trim());
|
|
332
|
+
const primaryFont = extractPrimaryFont(resolved);
|
|
333
|
+
if (primaryFont) {
|
|
334
|
+
style.fontFamily = primaryFont;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Text indent (e.g., "2rem", "-2rem" for hanging indents)
|
|
338
|
+
const textIndentMatch = ruleContent.match(/text-indent\s*:\s*([^;]+)/i);
|
|
339
|
+
if (textIndentMatch) {
|
|
340
|
+
style.textIndent = resolveValue(textIndentMatch[1].trim());
|
|
341
|
+
}
|
|
342
|
+
// Text transform (uppercase, lowercase, capitalize)
|
|
343
|
+
const textTransformMatch = ruleContent.match(/text-transform\s*:\s*([^;]+)/i);
|
|
344
|
+
if (textTransformMatch) {
|
|
345
|
+
style.textTransform = resolveValue(textTransformMatch[1].trim());
|
|
346
|
+
}
|
|
347
|
+
// GENERALIZED: Margin-bottom (for paragraph spacing)
|
|
348
|
+
const marginBottomMatch = ruleContent.match(/margin-bottom\s*:\s*([^;]+)/i);
|
|
349
|
+
if (marginBottomMatch) {
|
|
350
|
+
style.marginBottom = resolveValue(marginBottomMatch[1].trim());
|
|
351
|
+
}
|
|
352
|
+
// GENERALIZED: Line-height (for vertical spacing within text)
|
|
353
|
+
const lineHeightMatch = ruleContent.match(/line-height\s*:\s*([^;]+)/i);
|
|
354
|
+
if (lineHeightMatch) {
|
|
355
|
+
style.lineHeight = resolveValue(lineHeightMatch[1].trim());
|
|
356
|
+
}
|
|
357
|
+
// Letter-spacing (for character spacing)
|
|
358
|
+
const letterSpacingMatch = ruleContent.match(/letter-spacing\s*:\s*([^;]+)/i);
|
|
359
|
+
if (letterSpacingMatch) {
|
|
360
|
+
style.letterSpacing = resolveValue(letterSpacingMatch[1].trim());
|
|
361
|
+
}
|
|
362
|
+
// Margin-top (for heading spacing before)
|
|
363
|
+
const marginTopMatch = ruleContent.match(/margin-top\s*:\s*([^;]+)/i);
|
|
364
|
+
if (marginTopMatch) {
|
|
365
|
+
style.marginTop = resolveValue(marginTopMatch[1].trim());
|
|
366
|
+
}
|
|
367
|
+
// Store if we found any properties
|
|
368
|
+
// MERGE with existing styles (later rules can add properties without overwriting)
|
|
369
|
+
if (Object.keys(style).length > 0) {
|
|
370
|
+
for (const className of classNames) {
|
|
371
|
+
const existing = classStyles.get(className) || {};
|
|
372
|
+
classStyles.set(className, { ...existing, ...style });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Parse element.class combined selectors like "dd.dish-description { font-style: italic; }"
|
|
377
|
+
// These are more specific than just .class, but we store under the class name
|
|
378
|
+
// since getElementStyles already verifies element type separately
|
|
379
|
+
const elementClassMatches = cssText.matchAll(/(?:^|[;\n}])\s*([a-zA-Z][a-zA-Z0-9]*)\s*\.\s*([a-zA-Z0-9_-]+)\s*\{([^}]+)\}/gm);
|
|
380
|
+
for (const match of elementClassMatches) {
|
|
381
|
+
const elementName = match[1].toLowerCase();
|
|
382
|
+
const className = match[2];
|
|
383
|
+
const ruleContent = match[3];
|
|
384
|
+
// Extract style properties (same as class rules above)
|
|
385
|
+
const style = {};
|
|
386
|
+
// Font style (italic, normal)
|
|
387
|
+
const fontStyleMatch = ruleContent.match(/font-style\s*:\s*([^;]+)/i);
|
|
388
|
+
if (fontStyleMatch) {
|
|
389
|
+
style.fontStyle = resolveValue(fontStyleMatch[1].trim());
|
|
390
|
+
}
|
|
391
|
+
// Font weight
|
|
392
|
+
const fontWeightMatch = ruleContent.match(/font-weight\s*:\s*([^;]+)/i);
|
|
393
|
+
if (fontWeightMatch) {
|
|
394
|
+
style.fontWeight = resolveValue(fontWeightMatch[1].trim());
|
|
395
|
+
}
|
|
396
|
+
// Text color
|
|
397
|
+
const colorMatch = ruleContent.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
|
|
398
|
+
if (colorMatch) {
|
|
399
|
+
const colorValue = resolveValue(colorMatch[1].trim());
|
|
400
|
+
style.color = colorValue;
|
|
401
|
+
}
|
|
402
|
+
// Background color
|
|
403
|
+
const bgMatch = ruleContent.match(/background(?:-(?:color|image))?\s*:\s*([^;]+)/i);
|
|
404
|
+
if (bgMatch) {
|
|
405
|
+
const bgValue = resolveValue(bgMatch[1].trim());
|
|
406
|
+
// For gradient backgrounds, extract the first color as fallback for DOCX
|
|
407
|
+
// Also store the full gradient for containers that support gradient rendering
|
|
408
|
+
const gradientColor = extractFirstGradientColor(bgValue);
|
|
409
|
+
if (gradientColor) {
|
|
410
|
+
style.backgroundColor = gradientColor;
|
|
411
|
+
// Parse full gradient for container backgrounds
|
|
412
|
+
const gradient = parseGradient(bgValue);
|
|
413
|
+
if (gradient) {
|
|
414
|
+
style.backgroundGradient = gradient;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
style.backgroundColor = bgValue;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Text alignment
|
|
422
|
+
const textAlignMatch = ruleContent.match(/text-align\s*:\s*([^;]+)/i);
|
|
423
|
+
if (textAlignMatch) {
|
|
424
|
+
style.textAlign = resolveValue(textAlignMatch[1].trim());
|
|
425
|
+
}
|
|
426
|
+
// Font family (extract primary font from font stack)
|
|
427
|
+
const fontFamilyMatch = ruleContent.match(/font-family\s*:\s*([^;]+)/i);
|
|
428
|
+
if (fontFamilyMatch) {
|
|
429
|
+
const resolved = resolveValue(fontFamilyMatch[1].trim());
|
|
430
|
+
const primaryFont = extractPrimaryFont(resolved);
|
|
431
|
+
if (primaryFont) {
|
|
432
|
+
style.fontFamily = primaryFont;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Store under the class name (merge with existing)
|
|
436
|
+
if (Object.keys(style).length > 0) {
|
|
437
|
+
const existing = classStyles.get(className) || {};
|
|
438
|
+
classStyles.set(className, { ...existing, ...style });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Parse nested CSS selectors like ".parent .child { color: ... }" or ".parent element { color: ... }"
|
|
442
|
+
// Also handles "element.parent .child { ... }" like "figure.menu-image .image-placeholder { ... }"
|
|
443
|
+
// Store the parent-child relationship so we can look up styles based on context
|
|
444
|
+
// Supports: .works-cited p { text-indent: -2rem; } or .callout .callout-label { color: ... }
|
|
445
|
+
// Extended pattern to match:
|
|
446
|
+
// - .parentClass .child { ... }
|
|
447
|
+
// - element.parentClass .child { ... }
|
|
448
|
+
// - .parentClass tr:nth-child(even) { ... } (pseudo-selectors)
|
|
449
|
+
// - .parentClass .classA.classB { ... } (compound child selectors)
|
|
450
|
+
const nestedSelectorMatches = cssText.matchAll(/(?:[a-zA-Z0-9_-]*)?\.([a-zA-Z0-9_-]+)\s+(\.?[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*(?::[a-zA-Z0-9_-]+(?:\([^)]*\))?)?)\s*\{([^}]+)\}/g);
|
|
451
|
+
for (const match of nestedSelectorMatches) {
|
|
452
|
+
const parentClass = match[1];
|
|
453
|
+
let childSelector = match[2];
|
|
454
|
+
// Remove leading dot if present (for .class selectors)
|
|
455
|
+
if (childSelector.startsWith('.')) {
|
|
456
|
+
childSelector = childSelector.slice(1);
|
|
457
|
+
}
|
|
458
|
+
// Skip interactive pseudo-class selectors (:hover, :focus, :active, :visited)
|
|
459
|
+
// These states don't exist in static DOCX output
|
|
460
|
+
if (childSelector.match(/:(?:hover|focus|active|visited|focus-within|focus-visible)\b/)) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
// Strip pseudo-selectors for the map key (e.g., "tr:nth-child(even)" → "tr")
|
|
464
|
+
const baseChildSelector = childSelector.replace(/:.*$/, '');
|
|
465
|
+
const ruleContent = match[3];
|
|
466
|
+
// Extract style properties for the child when inside this parent
|
|
467
|
+
const style = {};
|
|
468
|
+
// Text color
|
|
469
|
+
const colorMatch = ruleContent.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
|
|
470
|
+
if (colorMatch) {
|
|
471
|
+
style.color = resolveValue(colorMatch[1].trim());
|
|
472
|
+
}
|
|
473
|
+
// Background color
|
|
474
|
+
const bgMatch = ruleContent.match(/background(?:-(?:color|image))?\s*:\s*([^;]+)/i);
|
|
475
|
+
if (bgMatch) {
|
|
476
|
+
const bgValue = resolveValue(bgMatch[1].trim());
|
|
477
|
+
// For gradient backgrounds, extract the first color as fallback for DOCX
|
|
478
|
+
// Also store the full gradient for containers that support gradient rendering
|
|
479
|
+
const gradientColor = extractFirstGradientColor(bgValue);
|
|
480
|
+
if (gradientColor) {
|
|
481
|
+
style.backgroundColor = gradientColor;
|
|
482
|
+
// Parse full gradient for container backgrounds
|
|
483
|
+
const gradient = parseGradient(bgValue);
|
|
484
|
+
if (gradient) {
|
|
485
|
+
style.backgroundGradient = gradient;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
style.backgroundColor = bgValue;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Gradient text detection: when background-clip: text is used with a gradient background,
|
|
493
|
+
// extract the first color from the gradient as fallback and also store full gradient
|
|
494
|
+
const hasBackgroundClipText = ruleContent.match(/(?:-webkit-)?background-clip\s*:\s*text/i);
|
|
495
|
+
if (hasBackgroundClipText && bgMatch) {
|
|
496
|
+
const bgValue = resolveValue(bgMatch[1].trim());
|
|
497
|
+
// Parse full gradient
|
|
498
|
+
const gradient = parseGradient(bgValue);
|
|
499
|
+
if (gradient) {
|
|
500
|
+
style.gradient = gradient;
|
|
501
|
+
// Clear backgroundColor since the gradient is for text fill, NOT paragraph background
|
|
502
|
+
style.backgroundColor = undefined;
|
|
503
|
+
style.backgroundGradient = undefined;
|
|
504
|
+
// Also set fallback color from first stop
|
|
505
|
+
if (!style.color) {
|
|
506
|
+
style.color = gradient.stops[0]?.color;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
// Fallback: extract first color if gradient parsing fails
|
|
511
|
+
const gradientColor = extractFirstGradientColor(bgValue);
|
|
512
|
+
if (gradientColor && !style.color) {
|
|
513
|
+
style.color = gradientColor;
|
|
514
|
+
}
|
|
515
|
+
// Clear backgroundColor for text gradient
|
|
516
|
+
style.backgroundColor = undefined;
|
|
517
|
+
style.backgroundGradient = undefined;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Font weight
|
|
521
|
+
const fontWeightMatch = ruleContent.match(/font-weight\s*:\s*([^;]+)/i);
|
|
522
|
+
if (fontWeightMatch) {
|
|
523
|
+
style.fontWeight = resolveValue(fontWeightMatch[1].trim());
|
|
524
|
+
}
|
|
525
|
+
// Text indent (for nested hanging indents like .works-cited p)
|
|
526
|
+
const textIndentMatch = ruleContent.match(/text-indent\s*:\s*([^;]+)/i);
|
|
527
|
+
if (textIndentMatch) {
|
|
528
|
+
style.textIndent = resolveValue(textIndentMatch[1].trim());
|
|
529
|
+
}
|
|
530
|
+
// Padding-left (for paragraph indentation in nested contexts)
|
|
531
|
+
const paddingLeftMatch = ruleContent.match(/padding-left\s*:\s*([^;]+)/i);
|
|
532
|
+
if (paddingLeftMatch) {
|
|
533
|
+
style.paddingLeft = resolveValue(paddingLeftMatch[1].trim());
|
|
534
|
+
}
|
|
535
|
+
// Padding shorthand (for nested selectors like .card td { padding: 0.75rem })
|
|
536
|
+
const paddingMatch = ruleContent.match(/(?:^|;)\s*padding\s*:\s*([^;]+)/i);
|
|
537
|
+
if (paddingMatch) {
|
|
538
|
+
style.padding = resolveValue(paddingMatch[1].trim());
|
|
539
|
+
}
|
|
540
|
+
// Individual padding properties (right, top, bottom)
|
|
541
|
+
const paddingRightMatch = ruleContent.match(/padding-right\s*:\s*([^;]+)/i);
|
|
542
|
+
if (paddingRightMatch) {
|
|
543
|
+
style.paddingRight = resolveValue(paddingRightMatch[1].trim());
|
|
544
|
+
}
|
|
545
|
+
const paddingTopMatch = ruleContent.match(/padding-top\s*:\s*([^;]+)/i);
|
|
546
|
+
if (paddingTopMatch) {
|
|
547
|
+
style.paddingTop = resolveValue(paddingTopMatch[1].trim());
|
|
548
|
+
}
|
|
549
|
+
const paddingBottomMatch = ruleContent.match(/padding-bottom\s*:\s*([^;]+)/i);
|
|
550
|
+
if (paddingBottomMatch) {
|
|
551
|
+
style.paddingBottom = resolveValue(paddingBottomMatch[1].trim());
|
|
552
|
+
}
|
|
553
|
+
// Font style
|
|
554
|
+
const fontStyleMatch = ruleContent.match(/font-style\s*:\s*([^;]+)/i);
|
|
555
|
+
if (fontStyleMatch) {
|
|
556
|
+
style.fontStyle = resolveValue(fontStyleMatch[1].trim());
|
|
557
|
+
}
|
|
558
|
+
// Font family (extract primary font from font stack)
|
|
559
|
+
const fontFamilyMatch = ruleContent.match(/font-family\s*:\s*([^;]+)/i);
|
|
560
|
+
if (fontFamilyMatch) {
|
|
561
|
+
const resolved = resolveValue(fontFamilyMatch[1].trim());
|
|
562
|
+
const primaryFont = extractPrimaryFont(resolved);
|
|
563
|
+
if (primaryFont) {
|
|
564
|
+
style.fontFamily = primaryFont;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Border (full shorthand, e.g., "1px solid #e5e7eb")
|
|
568
|
+
const borderMatch = ruleContent.match(/(?:^|;)\s*border\s*:\s*([^;]+)/i);
|
|
569
|
+
if (borderMatch) {
|
|
570
|
+
style.border = resolveValue(borderMatch[1].trim());
|
|
571
|
+
}
|
|
572
|
+
// Border-left (callout styling)
|
|
573
|
+
const borderLeftMatch = ruleContent.match(/border-left\s*:\s*([^;]+)/i);
|
|
574
|
+
if (borderLeftMatch) {
|
|
575
|
+
const borderLeftValue = resolveValue(borderLeftMatch[1].trim());
|
|
576
|
+
style.borderLeft = borderLeftValue;
|
|
577
|
+
// Extract color from border value
|
|
578
|
+
const colorInBorder = borderLeftValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
579
|
+
if (colorInBorder && !style.borderColor) {
|
|
580
|
+
style.borderColor = `#${colorInBorder[1]}`;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Border-bottom (heading underline styling, e.g., h2 { border-bottom: 3px solid #7c3aed; })
|
|
584
|
+
const borderBottomMatch = ruleContent.match(/border-bottom\s*:\s*([^;]+)/i);
|
|
585
|
+
if (borderBottomMatch) {
|
|
586
|
+
const borderBottomValue = resolveValue(borderBottomMatch[1].trim());
|
|
587
|
+
style.borderBottom = borderBottomValue;
|
|
588
|
+
// Extract color from border value
|
|
589
|
+
const colorInBorder = borderBottomValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
590
|
+
if (colorInBorder && !style.borderColor) {
|
|
591
|
+
style.borderColor = `#${colorInBorder[1]}`;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// Border-color (standalone property, e.g., .callout-important { border-color: #f59e0b; })
|
|
595
|
+
// This handles the pattern where border structure and color are split across rules:
|
|
596
|
+
// .callout { border-left: 4px solid; }
|
|
597
|
+
// .callout-important { border-color: #f59e0b; }
|
|
598
|
+
const borderColorMatch = ruleContent.match(/(?:^|;)\s*border-color\s*:\s*([^;]+)/i);
|
|
599
|
+
if (borderColorMatch) {
|
|
600
|
+
style.borderColor = resolveValue(borderColorMatch[1].trim());
|
|
601
|
+
}
|
|
602
|
+
// Border-top (e.g., .divider { border-top: 2px solid #e5e7eb; })
|
|
603
|
+
const borderTopMatch = ruleContent.match(/border-top\s*:\s*([^;]+)/i);
|
|
604
|
+
if (borderTopMatch) {
|
|
605
|
+
const borderTopValue = resolveValue(borderTopMatch[1].trim());
|
|
606
|
+
style.borderTop = borderTopValue;
|
|
607
|
+
const colorInBorder = borderTopValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
608
|
+
if (colorInBorder && !style.borderColor) {
|
|
609
|
+
style.borderColor = `#${colorInBorder[1]}`;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Border-right (for nested selectors like .container td { border-right: 1px solid #e5e7eb; })
|
|
613
|
+
const borderRightMatch = ruleContent.match(/border-right\s*:\s*([^;]+)/i);
|
|
614
|
+
if (borderRightMatch) {
|
|
615
|
+
const borderRightValue = resolveValue(borderRightMatch[1].trim());
|
|
616
|
+
style.borderRight = borderRightValue;
|
|
617
|
+
const colorInBorder = borderRightValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
618
|
+
if (colorInBorder && !style.borderColor) {
|
|
619
|
+
style.borderColor = `#${colorInBorder[1]}`;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Text-transform (uppercase, lowercase, capitalize for headings, labels, etc.)
|
|
623
|
+
const textTransformMatch = ruleContent.match(/text-transform\s*:\s*([^;]+)/i);
|
|
624
|
+
if (textTransformMatch) {
|
|
625
|
+
style.textTransform = resolveValue(textTransformMatch[1].trim());
|
|
626
|
+
}
|
|
627
|
+
// Text alignment (left, center, right, justify - for nested selectors like .body p { text-align: justify })
|
|
628
|
+
const textAlignMatch = ruleContent.match(/text-align\s*:\s*([^;]+)/i);
|
|
629
|
+
if (textAlignMatch) {
|
|
630
|
+
style.textAlign = resolveValue(textAlignMatch[1].trim());
|
|
631
|
+
}
|
|
632
|
+
// Font size (for nested selectors like .body p { font-size: 1.1rem })
|
|
633
|
+
const fontSizeMatch = ruleContent.match(/font-size\s*:\s*([^;]+)/i);
|
|
634
|
+
if (fontSizeMatch) {
|
|
635
|
+
style.fontSize = resolveValue(fontSizeMatch[1].trim());
|
|
636
|
+
}
|
|
637
|
+
// Margin-bottom (for paragraph spacing in nested selectors like .sender-info p { margin-bottom: 0.25rem })
|
|
638
|
+
const marginBottomMatch = ruleContent.match(/margin-bottom\s*:\s*([^;]+)/i);
|
|
639
|
+
if (marginBottomMatch) {
|
|
640
|
+
style.marginBottom = resolveValue(marginBottomMatch[1].trim());
|
|
641
|
+
}
|
|
642
|
+
// Line-height (for vertical spacing in nested selectors like .body p { line-height: 1.8 })
|
|
643
|
+
const lineHeightMatch = ruleContent.match(/line-height\s*:\s*([^;]+)/i);
|
|
644
|
+
if (lineHeightMatch) {
|
|
645
|
+
style.lineHeight = resolveValue(lineHeightMatch[1].trim());
|
|
646
|
+
}
|
|
647
|
+
// Letter-spacing (for character spacing in nested selectors like .header h1 { letter-spacing: 0.05em })
|
|
648
|
+
const letterSpacingMatch = ruleContent.match(/letter-spacing\s*:\s*([^;]+)/i);
|
|
649
|
+
if (letterSpacingMatch) {
|
|
650
|
+
style.letterSpacing = resolveValue(letterSpacingMatch[1].trim());
|
|
651
|
+
}
|
|
652
|
+
// Margin-top (for spacing before elements in nested selectors like .body h2 { margin-top: 1.5rem })
|
|
653
|
+
const marginTopMatch = ruleContent.match(/margin-top\s*:\s*([^;]+)/i);
|
|
654
|
+
if (marginTopMatch) {
|
|
655
|
+
style.marginTop = resolveValue(marginTopMatch[1].trim());
|
|
656
|
+
}
|
|
657
|
+
// Store in nestedStyles: parentClass -> (childSelector -> style)
|
|
658
|
+
// Use baseChildSelector (without pseudo-selectors) as the map key so that
|
|
659
|
+
// "tr:nth-child(even)" is stored under "tr" and can be found by table processing
|
|
660
|
+
if (Object.keys(style).length > 0) {
|
|
661
|
+
if (!nestedStyles.has(parentClass)) {
|
|
662
|
+
nestedStyles.set(parentClass, new Map());
|
|
663
|
+
}
|
|
664
|
+
const existing = nestedStyles.get(parentClass).get(baseChildSelector) || {};
|
|
665
|
+
nestedStyles.get(parentClass).set(baseChildSelector, { ...existing, ...style });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Parse element type selectors (body, p, h1, h2, h3, h4, h5, h6, etc.)
|
|
669
|
+
// These are rules like: body { color: var(--color-text); }
|
|
670
|
+
// or grouped rules like: h1, h2, h3, h4, h5, h6 { color: var(--color-heading); }
|
|
671
|
+
// IMPORTANT: Only match STANDALONE element selectors, not selectors like ".class p" or "#id p"
|
|
672
|
+
// The (?:^|[\n\r}]) lookbehind ensures we're after a rule boundary (not part of a complex selector)
|
|
673
|
+
// We need to be careful to not match ".timeline-content p" as just "p"
|
|
674
|
+
const elementSelectorPattern = /(?:^|[\n\r}])(\s*(?:body|p|h[1-6]|span|div|ul|ol|li|table|th|td|blockquote|section|article|aside|nav|header|footer|figure|figcaption|address|abbr|a)(?:\s*,\s*(?:body|p|h[1-6]|span|div|ul|ol|li|table|th|td|blockquote|section|article|aside|nav|header|footer|figure|figcaption|address|abbr|a))*)\s*\{([^}]+)\}/gi;
|
|
675
|
+
const elementMatches = cssText.matchAll(elementSelectorPattern);
|
|
676
|
+
for (const match of elementMatches) {
|
|
677
|
+
const selectorList = match[1];
|
|
678
|
+
const ruleContent = match[2];
|
|
679
|
+
// Split comma-separated selectors
|
|
680
|
+
const selectors = selectorList.split(/\s*,\s*/).map(s => s.trim().toLowerCase());
|
|
681
|
+
// Extract style properties
|
|
682
|
+
const style = {};
|
|
683
|
+
// Text color
|
|
684
|
+
const colorMatch = ruleContent.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
|
|
685
|
+
if (colorMatch) {
|
|
686
|
+
style.color = resolveValue(colorMatch[1].trim());
|
|
687
|
+
}
|
|
688
|
+
// Background color
|
|
689
|
+
const bgMatch = ruleContent.match(/background(?:-(?:color|image))?\s*:\s*([^;]+)/i);
|
|
690
|
+
if (bgMatch) {
|
|
691
|
+
const bgValue = resolveValue(bgMatch[1].trim());
|
|
692
|
+
// For gradient backgrounds, extract the first color as fallback for DOCX
|
|
693
|
+
// Also store the full gradient for containers that support gradient rendering
|
|
694
|
+
const gradientColor = extractFirstGradientColor(bgValue);
|
|
695
|
+
if (gradientColor) {
|
|
696
|
+
style.backgroundColor = gradientColor;
|
|
697
|
+
// Parse full gradient for container backgrounds
|
|
698
|
+
const gradient = parseGradient(bgValue);
|
|
699
|
+
if (gradient) {
|
|
700
|
+
style.backgroundGradient = gradient;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
style.backgroundColor = bgValue;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Font size
|
|
708
|
+
const fontSizeMatch = ruleContent.match(/font-size\s*:\s*([^;]+)/i);
|
|
709
|
+
if (fontSizeMatch) {
|
|
710
|
+
style.fontSize = resolveValue(fontSizeMatch[1].trim());
|
|
711
|
+
}
|
|
712
|
+
// Font weight
|
|
713
|
+
const fontWeightMatch = ruleContent.match(/font-weight\s*:\s*([^;]+)/i);
|
|
714
|
+
if (fontWeightMatch) {
|
|
715
|
+
style.fontWeight = resolveValue(fontWeightMatch[1].trim());
|
|
716
|
+
}
|
|
717
|
+
// Padding (for table cells th, td)
|
|
718
|
+
const paddingMatch = ruleContent.match(/(?:^|;)\s*padding\s*:\s*([^;]+)/i);
|
|
719
|
+
if (paddingMatch) {
|
|
720
|
+
style.padding = resolveValue(paddingMatch[1].trim());
|
|
721
|
+
}
|
|
722
|
+
// Individual padding properties
|
|
723
|
+
const paddingLeftMatch = ruleContent.match(/padding-left\s*:\s*([^;]+)/i);
|
|
724
|
+
if (paddingLeftMatch) {
|
|
725
|
+
style.paddingLeft = resolveValue(paddingLeftMatch[1].trim());
|
|
726
|
+
}
|
|
727
|
+
const paddingRightMatch = ruleContent.match(/padding-right\s*:\s*([^;]+)/i);
|
|
728
|
+
if (paddingRightMatch) {
|
|
729
|
+
style.paddingRight = resolveValue(paddingRightMatch[1].trim());
|
|
730
|
+
}
|
|
731
|
+
const paddingTopMatch = ruleContent.match(/padding-top\s*:\s*([^;]+)/i);
|
|
732
|
+
if (paddingTopMatch) {
|
|
733
|
+
style.paddingTop = resolveValue(paddingTopMatch[1].trim());
|
|
734
|
+
}
|
|
735
|
+
const paddingBottomMatch = ruleContent.match(/padding-bottom\s*:\s*([^;]+)/i);
|
|
736
|
+
if (paddingBottomMatch) {
|
|
737
|
+
style.paddingBottom = resolveValue(paddingBottomMatch[1].trim());
|
|
738
|
+
}
|
|
739
|
+
// Font style (italic, normal) - for blockquotes
|
|
740
|
+
const fontStyleMatch = ruleContent.match(/font-style\s*:\s*([^;]+)/i);
|
|
741
|
+
if (fontStyleMatch) {
|
|
742
|
+
style.fontStyle = resolveValue(fontStyleMatch[1].trim());
|
|
743
|
+
}
|
|
744
|
+
// Font family (extract primary font from font stack) - for body, headings, etc.
|
|
745
|
+
const fontFamilyMatch = ruleContent.match(/font-family\s*:\s*([^;]+)/i);
|
|
746
|
+
if (fontFamilyMatch) {
|
|
747
|
+
const resolved = resolveValue(fontFamilyMatch[1].trim());
|
|
748
|
+
const primaryFont = extractPrimaryFont(resolved);
|
|
749
|
+
if (primaryFont) {
|
|
750
|
+
style.fontFamily = primaryFont;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
// Text indent (for paragraph first-line indentation)
|
|
754
|
+
const textIndentMatch = ruleContent.match(/text-indent\s*:\s*([^;]+)/i);
|
|
755
|
+
if (textIndentMatch) {
|
|
756
|
+
style.textIndent = resolveValue(textIndentMatch[1].trim());
|
|
757
|
+
}
|
|
758
|
+
// Text transform (uppercase, lowercase, capitalize)
|
|
759
|
+
const textTransformMatch = ruleContent.match(/text-transform\s*:\s*([^;]+)/i);
|
|
760
|
+
if (textTransformMatch) {
|
|
761
|
+
style.textTransform = resolveValue(textTransformMatch[1].trim());
|
|
762
|
+
}
|
|
763
|
+
// GENERALIZED: Margin-bottom (for paragraph spacing)
|
|
764
|
+
const marginBottomMatch = ruleContent.match(/margin-bottom\s*:\s*([^;]+)/i);
|
|
765
|
+
if (marginBottomMatch) {
|
|
766
|
+
style.marginBottom = resolveValue(marginBottomMatch[1].trim());
|
|
767
|
+
}
|
|
768
|
+
// GENERALIZED: Line-height (for vertical spacing within text)
|
|
769
|
+
const lineHeightMatch = ruleContent.match(/line-height\s*:\s*([^;]+)/i);
|
|
770
|
+
if (lineHeightMatch) {
|
|
771
|
+
style.lineHeight = resolveValue(lineHeightMatch[1].trim());
|
|
772
|
+
}
|
|
773
|
+
// Letter-spacing (for character spacing on element selectors like h1, h2, p)
|
|
774
|
+
const letterSpacingMatch = ruleContent.match(/letter-spacing\s*:\s*([^;]+)/i);
|
|
775
|
+
if (letterSpacingMatch) {
|
|
776
|
+
style.letterSpacing = resolveValue(letterSpacingMatch[1].trim());
|
|
777
|
+
}
|
|
778
|
+
// Margin-top (for heading spacing before)
|
|
779
|
+
const marginTopMatch = ruleContent.match(/margin-top\s*:\s*([^;]+)/i);
|
|
780
|
+
if (marginTopMatch) {
|
|
781
|
+
style.marginTop = resolveValue(marginTopMatch[1].trim());
|
|
782
|
+
}
|
|
783
|
+
// Text alignment (left, center, right, justify)
|
|
784
|
+
const textAlignMatch = ruleContent.match(/text-align\s*:\s*([^;]+)/i);
|
|
785
|
+
if (textAlignMatch) {
|
|
786
|
+
style.textAlign = resolveValue(textAlignMatch[1].trim());
|
|
787
|
+
}
|
|
788
|
+
// Border shorthand (e.g., "1px solid #e5e7eb")
|
|
789
|
+
const borderMatch = ruleContent.match(/(?:^|;)\s*border\s*:\s*([^;]+)/i);
|
|
790
|
+
if (borderMatch) {
|
|
791
|
+
style.border = resolveValue(borderMatch[1].trim());
|
|
792
|
+
}
|
|
793
|
+
// Border-left (used by blockquotes/callouts: "4px solid #8b4513")
|
|
794
|
+
const borderLeftMatch = ruleContent.match(/border-left\s*:\s*([^;]+)/i);
|
|
795
|
+
if (borderLeftMatch) {
|
|
796
|
+
const borderLeftValue = resolveValue(borderLeftMatch[1].trim());
|
|
797
|
+
// If it contains a color, extract it as borderColor
|
|
798
|
+
const colorInBorder = borderLeftValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
799
|
+
if (colorInBorder && !style.borderColor) {
|
|
800
|
+
style.borderColor = `#${colorInBorder[1]}`;
|
|
801
|
+
}
|
|
802
|
+
// Store the full border-left value
|
|
803
|
+
style.borderLeft = borderLeftValue;
|
|
804
|
+
}
|
|
805
|
+
// Border-right (used by sidebar dividers, etc.)
|
|
806
|
+
const borderRightMatch = ruleContent.match(/border-right\s*:\s*([^;]+)/i);
|
|
807
|
+
if (borderRightMatch) {
|
|
808
|
+
style.borderRight = resolveValue(borderRightMatch[1].trim());
|
|
809
|
+
}
|
|
810
|
+
// Border color (direct property)
|
|
811
|
+
const borderColorMatch = ruleContent.match(/border-color\s*:\s*([^;]+)/i);
|
|
812
|
+
if (borderColorMatch) {
|
|
813
|
+
style.borderColor = resolveValue(borderColorMatch[1].trim());
|
|
814
|
+
}
|
|
815
|
+
// GENERALIZED: Border-bottom (used by h2 underlines, title blocks, etc.)
|
|
816
|
+
// Any element can have border-bottom - extract from element selectors
|
|
817
|
+
const borderBottomMatch = ruleContent.match(/border-bottom\s*:\s*([^;]+)/i);
|
|
818
|
+
if (borderBottomMatch) {
|
|
819
|
+
style.borderBottom = resolveValue(borderBottomMatch[1].trim());
|
|
820
|
+
}
|
|
821
|
+
// GENERALIZED: Border-top (used by footer sections with top divider lines)
|
|
822
|
+
// Any element can have border-top - extract from element selectors
|
|
823
|
+
const borderTopMatch = ruleContent.match(/border-top\s*:\s*([^;]+)/i);
|
|
824
|
+
if (borderTopMatch) {
|
|
825
|
+
style.borderTop = resolveValue(borderTopMatch[1].trim());
|
|
826
|
+
}
|
|
827
|
+
// Apply style to each selector
|
|
828
|
+
if (Object.keys(style).length > 0) {
|
|
829
|
+
for (const selector of selectors) {
|
|
830
|
+
// Merge with existing styles (later rules override)
|
|
831
|
+
const existing = elementStyles.get(selector) || {};
|
|
832
|
+
elementStyles.set(selector, { ...existing, ...style });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
// Parse element-to-element nested selectors like "thead th { ... }" or "tbody td { ... }"
|
|
837
|
+
// These are compound selectors with a parent element and a child element
|
|
838
|
+
// We store them in nestedStyles using "__elem:parentTag" as the key
|
|
839
|
+
// so they can be looked up by ancestor element type in table/heading parsing
|
|
840
|
+
const elementElements = ["thead", "tbody", "tfoot", "table", "tr", "ul", "ol", "nav", "header", "footer", "section", "article", "aside", "figure", "blockquote", "div"];
|
|
841
|
+
const childElements = ["th", "td", "tr", "li", "p", "h1", "h2", "h3", "h4", "h5", "h6", "span", "a", "img", "div"];
|
|
842
|
+
const parentList = elementElements.join("|");
|
|
843
|
+
const childList = childElements.join("|");
|
|
844
|
+
const elemElemPattern = new RegExp(`(?:^|[\\n\\r])\\s*((?:${parentList})(?:\\s*,\\s*(?:${parentList}))*)\\s+((?:${childList})(?:\\s*,\\s*(?:${childList}))*)\\s*\\{([^}]+)\\}`, "gi");
|
|
845
|
+
const elemElemMatches = cssText.matchAll(elemElemPattern);
|
|
846
|
+
for (const match of elemElemMatches) {
|
|
847
|
+
const parentSelectors = match[1].split(/\s*,\s*/).map(s => s.trim().toLowerCase());
|
|
848
|
+
const childSelectors = match[2].split(/\s*,\s*/).map(s => s.trim().toLowerCase());
|
|
849
|
+
const ruleContent = match[3];
|
|
850
|
+
// Extract style properties
|
|
851
|
+
const style = {};
|
|
852
|
+
const colorMatch = ruleContent.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
|
|
853
|
+
if (colorMatch)
|
|
854
|
+
style.color = resolveValue(colorMatch[1].trim());
|
|
855
|
+
const bgMatch = ruleContent.match(/background(?:-(?:color|image))?\s*:\s*([^;]+)/i);
|
|
856
|
+
if (bgMatch) {
|
|
857
|
+
const bgValue = resolveValue(bgMatch[1].trim());
|
|
858
|
+
const gradientColor = extractFirstGradientColor(bgValue);
|
|
859
|
+
style.backgroundColor = gradientColor || bgValue;
|
|
860
|
+
}
|
|
861
|
+
const fontWeightMatch = ruleContent.match(/font-weight\s*:\s*([^;]+)/i);
|
|
862
|
+
if (fontWeightMatch)
|
|
863
|
+
style.fontWeight = resolveValue(fontWeightMatch[1].trim());
|
|
864
|
+
const fontSizeMatch = ruleContent.match(/font-size\s*:\s*([^;]+)/i);
|
|
865
|
+
if (fontSizeMatch)
|
|
866
|
+
style.fontSize = resolveValue(fontSizeMatch[1].trim());
|
|
867
|
+
const paddingMatch = ruleContent.match(/(?:^|;)\s*padding\s*:\s*([^;]+)/i);
|
|
868
|
+
if (paddingMatch)
|
|
869
|
+
style.padding = resolveValue(paddingMatch[1].trim());
|
|
870
|
+
const textAlignMatch = ruleContent.match(/text-align\s*:\s*([^;]+)/i);
|
|
871
|
+
if (textAlignMatch)
|
|
872
|
+
style.textAlign = resolveValue(textAlignMatch[1].trim());
|
|
873
|
+
const borderBottomMatch = ruleContent.match(/border-bottom\s*:\s*([^;]+)/i);
|
|
874
|
+
if (borderBottomMatch)
|
|
875
|
+
style.borderBottom = resolveValue(borderBottomMatch[1].trim());
|
|
876
|
+
if (Object.keys(style).length > 0) {
|
|
877
|
+
for (const parent of parentSelectors) {
|
|
878
|
+
// Store using "__elem:" prefix to distinguish from class-based nesting
|
|
879
|
+
const key = `__elem:${parent}`;
|
|
880
|
+
for (const child of childSelectors) {
|
|
881
|
+
const existing = nestedStyles.get(key) || new Map();
|
|
882
|
+
const existingChild = existing.get(child) || {};
|
|
883
|
+
existing.set(child, { ...existingChild, ...style });
|
|
884
|
+
nestedStyles.set(key, existing);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// Parse element-with-pseudo-selector nested patterns like:
|
|
890
|
+
// tbody tr:nth-child(even) { background: #f9fafb; }
|
|
891
|
+
// table tr:nth-child(even) { background-color: #f5f5f5; }
|
|
892
|
+
// .my-table tr:nth-child(even) { background: ... }
|
|
893
|
+
// These are stored as "__even-row" in elementStyles so table parsing can retrieve them
|
|
894
|
+
const evenRowPattern = /(?:^|[\n\r])\s*(?:(?:tbody|table|\.[\w-]+)\s+)?tr\s*:\s*nth-child\s*\(\s*even\s*\)\s*\{([^}]+)\}/gi;
|
|
895
|
+
const evenRowMatches = cssText.matchAll(evenRowPattern);
|
|
896
|
+
for (const match of evenRowMatches) {
|
|
897
|
+
const ruleContent = match[1];
|
|
898
|
+
const bgMatch = ruleContent.match(/background(?:-color)?\s*:\s*([^;]+)/i);
|
|
899
|
+
if (bgMatch) {
|
|
900
|
+
const bgValue = resolveValue(bgMatch[1].trim());
|
|
901
|
+
const hex = extractHexColor(bgValue);
|
|
902
|
+
if (hex) {
|
|
903
|
+
// Store as a special element style that table parsing can look up
|
|
904
|
+
elementStyles.set("__even-row", { backgroundColor: `#${hex}` });
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return { variables, classColors, calloutStyles, classStyles, elementStyles, nestedStyles };
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Get merged styles for an element by combining all its class styles.
|
|
913
|
+
* Later classes override earlier ones.
|
|
914
|
+
* Also checks element type selectors as a base layer.
|
|
915
|
+
*
|
|
916
|
+
* @param element The element to get styles for
|
|
917
|
+
* @param cssContext The CSS context
|
|
918
|
+
* @param parentElement Optional parent element for nested style lookups
|
|
919
|
+
*/
|
|
920
|
+
export function getElementStyles(element, cssContext, parentElement) {
|
|
921
|
+
const result = {};
|
|
922
|
+
// CSS Inheritance: inherit certain properties from body styles
|
|
923
|
+
// In CSS, properties like line-height, color, font-family, font-size, letter-spacing
|
|
924
|
+
// are inherited from ancestor elements. Since our parser doesn't walk the full DOM tree
|
|
925
|
+
// for inheritance, we manually inherit from body styles as a baseline.
|
|
926
|
+
const bodyStyle = cssContext.elementStyles.get("body");
|
|
927
|
+
if (bodyStyle) {
|
|
928
|
+
// Only inherit CSS-inheritable properties, not box-model properties
|
|
929
|
+
if (bodyStyle.lineHeight)
|
|
930
|
+
result.lineHeight = bodyStyle.lineHeight;
|
|
931
|
+
if (bodyStyle.color)
|
|
932
|
+
result.color = bodyStyle.color;
|
|
933
|
+
if (bodyStyle.fontFamily)
|
|
934
|
+
result.fontFamily = bodyStyle.fontFamily;
|
|
935
|
+
if (bodyStyle.fontSize)
|
|
936
|
+
result.fontSize = bodyStyle.fontSize;
|
|
937
|
+
if (bodyStyle.letterSpacing)
|
|
938
|
+
result.letterSpacing = bodyStyle.letterSpacing;
|
|
939
|
+
}
|
|
940
|
+
// First, apply element type selector styles (lowest priority, overrides body inheritance)
|
|
941
|
+
const tagName = element.tagName.toLowerCase();
|
|
942
|
+
const elementTypeStyle = cssContext.elementStyles.get(tagName);
|
|
943
|
+
if (elementTypeStyle) {
|
|
944
|
+
Object.assign(result, elementTypeStyle);
|
|
945
|
+
}
|
|
946
|
+
// Then apply class styles (higher priority)
|
|
947
|
+
const classAttr = element.getAttribute("class");
|
|
948
|
+
const elementClasses = classAttr ? classAttr.split(/\s+/).filter(c => c.length > 0) : [];
|
|
949
|
+
for (const className of elementClasses) {
|
|
950
|
+
const classStyle = cssContext.classStyles.get(className);
|
|
951
|
+
if (classStyle) {
|
|
952
|
+
Object.assign(result, classStyle);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// Apply compound class styles (even higher specificity than single class)
|
|
956
|
+
// Check all compound keys in classStyles where the element has ALL required classes
|
|
957
|
+
if (elementClasses.length >= 2) {
|
|
958
|
+
for (const [key, compoundStyle] of cssContext.classStyles) {
|
|
959
|
+
if (key.includes('.')) {
|
|
960
|
+
const requiredClasses = key.split('.');
|
|
961
|
+
if (requiredClasses.every(c => elementClasses.includes(c))) {
|
|
962
|
+
Object.assign(result, compoundStyle);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// Apply nested styles if parent is provided (highest CSS priority for nested selectors)
|
|
968
|
+
// This handles rules like ".parent .child { color: ... }" and ".parent element { color: ... }"
|
|
969
|
+
if (parentElement) {
|
|
970
|
+
const parentClassAttr = parentElement.getAttribute("class");
|
|
971
|
+
const parentClasses = parentClassAttr ? parentClassAttr.split(/\s+/).filter(c => c.length > 0) : [];
|
|
972
|
+
// First, inherit text color from parent's class styles (CSS color inheritance)
|
|
973
|
+
// This handles cases like ".cta { color: white; }" where children should inherit the color
|
|
974
|
+
if (!result.color) {
|
|
975
|
+
for (const parentClass of parentClasses) {
|
|
976
|
+
const parentClassStyle = cssContext.classStyles.get(parentClass);
|
|
977
|
+
if (parentClassStyle?.color) {
|
|
978
|
+
result.color = parentClassStyle.color;
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// For each parent class, check if there are nested styles for our element's classes OR tag name
|
|
984
|
+
for (const parentClass of parentClasses) {
|
|
985
|
+
const nestedMap = cssContext.nestedStyles.get(parentClass);
|
|
986
|
+
if (nestedMap) {
|
|
987
|
+
// Check for element tag name (e.g., .works-cited p { ... })
|
|
988
|
+
const nestedTagStyle = nestedMap.get(tagName);
|
|
989
|
+
if (nestedTagStyle) {
|
|
990
|
+
Object.assign(result, nestedTagStyle);
|
|
991
|
+
}
|
|
992
|
+
// Check for element's classes (simple selectors)
|
|
993
|
+
for (const childClass of elementClasses) {
|
|
994
|
+
const nestedStyle = nestedMap.get(childClass);
|
|
995
|
+
if (nestedStyle) {
|
|
996
|
+
Object.assign(result, nestedStyle);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// Check for compound child selectors (e.g., .parent .classA.classB)
|
|
1000
|
+
if (elementClasses.length >= 2) {
|
|
1001
|
+
for (const [childKey, nestedStyle] of nestedMap) {
|
|
1002
|
+
if (childKey.includes('.')) {
|
|
1003
|
+
const requiredClasses = childKey.split('.');
|
|
1004
|
+
if (requiredClasses.every(c => elementClasses.includes(c))) {
|
|
1005
|
+
Object.assign(result, nestedStyle);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// Also walk up the DOM tree to find ancestor containers with nested styles
|
|
1014
|
+
// This handles cases where the parent element isn't passed explicitly
|
|
1015
|
+
// IMPORTANT: Closer ancestors have higher specificity than distant ancestors,
|
|
1016
|
+
// so we use a "fill-in" approach - only set properties not already set by closer ancestors
|
|
1017
|
+
if (!parentElement) {
|
|
1018
|
+
// Helper: merge only properties that aren't already set in result
|
|
1019
|
+
const fillIn = (source) => {
|
|
1020
|
+
for (const [key, value] of Object.entries(source)) {
|
|
1021
|
+
if (value !== undefined && result[key] === undefined) {
|
|
1022
|
+
result[key] = value;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
// Track whether we've already applied a nested class→tag override.
|
|
1027
|
+
// The FIRST (closest) ancestor's class→tag style should override bare element-type
|
|
1028
|
+
// styles (e.g., `.comparison-table th` overrides `th`), because .class element
|
|
1029
|
+
// has higher CSS specificity (0,1,1) than element (0,0,1).
|
|
1030
|
+
// Subsequent (more distant) ancestors use fillIn to avoid overriding closer ones.
|
|
1031
|
+
let appliedNestedTagOverride = false;
|
|
1032
|
+
let ancestor = element.parentElement;
|
|
1033
|
+
while (ancestor) {
|
|
1034
|
+
const ancestorClassAttr = ancestor.getAttribute("class");
|
|
1035
|
+
const ancestorClasses = ancestorClassAttr ? ancestorClassAttr.split(/\s+/).filter(c => c.length > 0) : [];
|
|
1036
|
+
for (const ancestorClass of ancestorClasses) {
|
|
1037
|
+
const nestedMap = cssContext.nestedStyles.get(ancestorClass);
|
|
1038
|
+
if (nestedMap) {
|
|
1039
|
+
// Check for element tag name (e.g., .comparison-table th { ... })
|
|
1040
|
+
// CSS specificity: .class element (0,1,1) > element (0,0,1)
|
|
1041
|
+
// So nested class→tag styles OVERRIDE bare element-type styles.
|
|
1042
|
+
// Only the closest ancestor's class→tag style should override; further
|
|
1043
|
+
// ancestors fill in only properties not yet set.
|
|
1044
|
+
const nestedTagStyle = nestedMap.get(tagName);
|
|
1045
|
+
if (nestedTagStyle) {
|
|
1046
|
+
if (!appliedNestedTagOverride) {
|
|
1047
|
+
Object.assign(result, nestedTagStyle);
|
|
1048
|
+
appliedNestedTagOverride = true;
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
fillIn(nestedTagStyle);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// Check for element's classes (simple selectors)
|
|
1055
|
+
for (const childClass of elementClasses) {
|
|
1056
|
+
const nestedStyle = nestedMap.get(childClass);
|
|
1057
|
+
if (nestedStyle) {
|
|
1058
|
+
fillIn(nestedStyle);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// Check for compound child selectors (e.g., .ancestor .classA.classB)
|
|
1062
|
+
if (elementClasses.length >= 2) {
|
|
1063
|
+
for (const [childKey, nestedStyle] of nestedMap) {
|
|
1064
|
+
if (childKey.includes('.')) {
|
|
1065
|
+
const requiredClasses = childKey.split('.');
|
|
1066
|
+
if (requiredClasses.every(c => elementClasses.includes(c))) {
|
|
1067
|
+
fillIn(nestedStyle);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
// Check element-to-element nested styles (e.g., "thead th { ... }", "tbody td { ... }")
|
|
1075
|
+
// These are stored with "__elem:" prefix in nestedStyles
|
|
1076
|
+
const ancestorTagName2 = ancestor.tagName?.toLowerCase();
|
|
1077
|
+
if (ancestorTagName2) {
|
|
1078
|
+
const elemElemMap = cssContext.nestedStyles.get(`__elem:${ancestorTagName2}`);
|
|
1079
|
+
if (elemElemMap) {
|
|
1080
|
+
const nestedTagStyle = elemElemMap.get(tagName);
|
|
1081
|
+
if (nestedTagStyle) {
|
|
1082
|
+
fillIn(nestedTagStyle);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
// CSS color inheritance: if result doesn't have a color yet, check if ancestor has one
|
|
1087
|
+
// CSS `color` is an inherited property - children inherit it from parents
|
|
1088
|
+
if (!result.color) {
|
|
1089
|
+
for (const ancestorClass of ancestorClasses) {
|
|
1090
|
+
const ancestorClassStyle = cssContext.classStyles.get(ancestorClass);
|
|
1091
|
+
if (ancestorClassStyle?.color) {
|
|
1092
|
+
result.color = ancestorClassStyle.color;
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
// Also check ancestor's element type selector for color (e.g., body { color: #333 })
|
|
1097
|
+
if (!result.color) {
|
|
1098
|
+
const ancestorTagName = ancestor.tagName?.toLowerCase();
|
|
1099
|
+
if (ancestorTagName) {
|
|
1100
|
+
const ancestorTypeStyle = cssContext.elementStyles.get(ancestorTagName);
|
|
1101
|
+
if (ancestorTypeStyle?.color) {
|
|
1102
|
+
result.color = ancestorTypeStyle.color;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
// Also check ancestor's inline style for color
|
|
1107
|
+
if (!result.color) {
|
|
1108
|
+
const ancestorInline = ancestor.getAttribute("style") || "";
|
|
1109
|
+
const ancestorColorMatch = ancestorInline.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
|
|
1110
|
+
if (ancestorColorMatch) {
|
|
1111
|
+
result.color = ancestorColorMatch[1].trim();
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
ancestor = ancestor.parentElement;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Inherit font-family from ancestor elements if not set on this element
|
|
1119
|
+
// CSS font-family is an inherited property, so we need to walk up the DOM tree
|
|
1120
|
+
if (!result.fontFamily) {
|
|
1121
|
+
let ancestor = element.parentElement;
|
|
1122
|
+
while (ancestor && !result.fontFamily) {
|
|
1123
|
+
const ancestorTagName = ancestor.tagName?.toLowerCase();
|
|
1124
|
+
if (ancestorTagName) {
|
|
1125
|
+
// Check element type selector for ancestor (e.g., body { font-family: ... })
|
|
1126
|
+
const ancestorTypeStyle = cssContext.elementStyles.get(ancestorTagName);
|
|
1127
|
+
if (ancestorTypeStyle?.fontFamily) {
|
|
1128
|
+
result.fontFamily = ancestorTypeStyle.fontFamily;
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
// Check ancestor's class styles
|
|
1132
|
+
const ancestorClassAttr = ancestor.getAttribute("class");
|
|
1133
|
+
const ancestorClasses = ancestorClassAttr ? ancestorClassAttr.split(/\s+/).filter(c => c.length > 0) : [];
|
|
1134
|
+
for (const ancestorClass of ancestorClasses) {
|
|
1135
|
+
const classStyle = cssContext.classStyles.get(ancestorClass);
|
|
1136
|
+
if (classStyle?.fontFamily) {
|
|
1137
|
+
result.fontFamily = classStyle.fontFamily;
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
ancestor = ancestor.parentElement;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// GENERALIZED: Inherit line-height from ancestor elements if not set on this element
|
|
1146
|
+
// CSS line-height is an inherited property (like font-family and color)
|
|
1147
|
+
if (!result.lineHeight) {
|
|
1148
|
+
let ancestor = element.parentElement;
|
|
1149
|
+
while (ancestor && !result.lineHeight) {
|
|
1150
|
+
const ancestorTagName = ancestor.tagName?.toLowerCase();
|
|
1151
|
+
if (ancestorTagName) {
|
|
1152
|
+
const ancestorTypeStyle = cssContext.elementStyles.get(ancestorTagName);
|
|
1153
|
+
if (ancestorTypeStyle?.lineHeight) {
|
|
1154
|
+
result.lineHeight = ancestorTypeStyle.lineHeight;
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
const ancestorClassAttr = ancestor.getAttribute("class");
|
|
1158
|
+
const ancestorClasses = ancestorClassAttr ? ancestorClassAttr.split(/\s+/).filter(c => c.length > 0) : [];
|
|
1159
|
+
for (const ancestorClass of ancestorClasses) {
|
|
1160
|
+
const classStyle = cssContext.classStyles.get(ancestorClass);
|
|
1161
|
+
if (classStyle?.lineHeight) {
|
|
1162
|
+
result.lineHeight = classStyle.lineHeight;
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
ancestor = ancestor.parentElement;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// Note: text-align inheritance is handled by the caller (processNode's inheritedAlignment,
|
|
1171
|
+
// or parseBlockquoteContent's explicit blockquoteStyles.textAlign fallback).
|
|
1172
|
+
// We don't inherit text-align in getElementStyles to avoid double-inheritance in the main path.
|
|
1173
|
+
// Also check inline styles, which override CSS classes
|
|
1174
|
+
const inlineStyle = element.getAttribute("style") || "";
|
|
1175
|
+
if (inlineStyle) {
|
|
1176
|
+
const bgMatch = inlineStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i);
|
|
1177
|
+
if (bgMatch)
|
|
1178
|
+
result.backgroundColor = bgMatch[1].trim();
|
|
1179
|
+
const colorMatch = inlineStyle.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
|
|
1180
|
+
if (colorMatch)
|
|
1181
|
+
result.color = colorMatch[1].trim();
|
|
1182
|
+
const borderMatch = inlineStyle.match(/(?:^|;)\s*border\s*:\s*([^;]+)/i);
|
|
1183
|
+
if (borderMatch)
|
|
1184
|
+
result.border = borderMatch[1].trim();
|
|
1185
|
+
const borderColorMatch = inlineStyle.match(/border-color\s*:\s*([^;]+)/i);
|
|
1186
|
+
if (borderColorMatch)
|
|
1187
|
+
result.borderColor = borderColorMatch[1].trim();
|
|
1188
|
+
const borderLeftMatch = inlineStyle.match(/border-left\s*:\s*([^;]+)/i);
|
|
1189
|
+
if (borderLeftMatch)
|
|
1190
|
+
result.borderLeft = borderLeftMatch[1].trim();
|
|
1191
|
+
const borderBottomMatch = inlineStyle.match(/border-bottom\s*:\s*([^;]+)/i);
|
|
1192
|
+
if (borderBottomMatch)
|
|
1193
|
+
result.borderBottom = borderBottomMatch[1].trim();
|
|
1194
|
+
const displayMatch = inlineStyle.match(/display\s*:\s*([^;]+)/i);
|
|
1195
|
+
if (displayMatch)
|
|
1196
|
+
result.display = displayMatch[1].trim();
|
|
1197
|
+
const flexMatch = inlineStyle.match(/(?:^|;)\s*flex\s*:\s*([^;]+)/i);
|
|
1198
|
+
if (flexMatch)
|
|
1199
|
+
result.flex = flexMatch[1].trim();
|
|
1200
|
+
const gridColsMatch = inlineStyle.match(/grid-template-columns\s*:\s*([^;]+)/i);
|
|
1201
|
+
if (gridColsMatch)
|
|
1202
|
+
result.gridTemplateColumns = gridColsMatch[1].trim();
|
|
1203
|
+
const textAlignMatch = inlineStyle.match(/text-align\s*:\s*([^;]+)/i);
|
|
1204
|
+
if (textAlignMatch)
|
|
1205
|
+
result.textAlign = textAlignMatch[1].trim();
|
|
1206
|
+
// Inline font-family (highest priority)
|
|
1207
|
+
const fontFamilyMatch = inlineStyle.match(/font-family\s*:\s*([^;]+)/i);
|
|
1208
|
+
if (fontFamilyMatch) {
|
|
1209
|
+
const primaryFont = extractPrimaryFont(fontFamilyMatch[1].trim());
|
|
1210
|
+
if (primaryFont)
|
|
1211
|
+
result.fontFamily = primaryFont;
|
|
1212
|
+
}
|
|
1213
|
+
// Inline line-height
|
|
1214
|
+
const lineHeightMatch = inlineStyle.match(/line-height\s*:\s*([^;]+)/i);
|
|
1215
|
+
if (lineHeightMatch) {
|
|
1216
|
+
result.lineHeight = lineHeightMatch[1].trim();
|
|
1217
|
+
}
|
|
1218
|
+
// Inline margin-bottom
|
|
1219
|
+
const marginBottomInlineMatch = inlineStyle.match(/margin-bottom\s*:\s*([^;]+)/i);
|
|
1220
|
+
if (marginBottomInlineMatch) {
|
|
1221
|
+
result.marginBottom = marginBottomInlineMatch[1].trim();
|
|
1222
|
+
}
|
|
1223
|
+
// Inline gradient text detection: background-clip: text + gradient background
|
|
1224
|
+
// This handles inline styles like:
|
|
1225
|
+
// style="background: linear-gradient(90deg, #ff0, #f00); -webkit-background-clip: text; color: transparent;"
|
|
1226
|
+
const hasInlineBackgroundClipText = inlineStyle.match(/(?:-webkit-)?background-clip\s*:\s*text/i);
|
|
1227
|
+
if (hasInlineBackgroundClipText) {
|
|
1228
|
+
const bgInlineMatch = inlineStyle.match(/background(?:-image)?\s*:\s*(linear-gradient\s*\([^)]+(?:\([^)]*\))*[^)]*\))/i);
|
|
1229
|
+
if (bgInlineMatch) {
|
|
1230
|
+
const gradient = parseGradient(bgInlineMatch[1]);
|
|
1231
|
+
if (gradient) {
|
|
1232
|
+
result.gradient = gradient;
|
|
1233
|
+
// Clear backgroundColor since the gradient is for text fill, NOT paragraph background
|
|
1234
|
+
result.backgroundColor = undefined;
|
|
1235
|
+
// Set fallback color from first stop
|
|
1236
|
+
if (!result.color || result.color === "transparent") {
|
|
1237
|
+
result.color = gradient.stops[0]?.color;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
// Inline font-style (italic)
|
|
1243
|
+
const fontStyleMatch = inlineStyle.match(/font-style\s*:\s*([^;]+)/i);
|
|
1244
|
+
if (fontStyleMatch)
|
|
1245
|
+
result.fontStyle = fontStyleMatch[1].trim();
|
|
1246
|
+
// Inline font-weight (bold)
|
|
1247
|
+
const fontWeightMatch = inlineStyle.match(/font-weight\s*:\s*([^;]+)/i);
|
|
1248
|
+
if (fontWeightMatch)
|
|
1249
|
+
result.fontWeight = fontWeightMatch[1].trim();
|
|
1250
|
+
// Inline text-transform
|
|
1251
|
+
const textTransformMatch = inlineStyle.match(/text-transform\s*:\s*([^;]+)/i);
|
|
1252
|
+
if (textTransformMatch)
|
|
1253
|
+
result.textTransform = textTransformMatch[1].trim();
|
|
1254
|
+
}
|
|
1255
|
+
// GENERALIZED: Use getComputedStyle as fallback for font-family and line-height
|
|
1256
|
+
// This ensures we get the ACTUAL computed values from Playwright's browser context,
|
|
1257
|
+
// including all CSS variable resolution and inheritance
|
|
1258
|
+
if (typeof window !== "undefined" && window.getComputedStyle) {
|
|
1259
|
+
try {
|
|
1260
|
+
const computed = window.getComputedStyle(element);
|
|
1261
|
+
// Font-family: Use computed style if not already set from CSS parsing
|
|
1262
|
+
if (!result.fontFamily && computed.fontFamily) {
|
|
1263
|
+
const primaryFont = extractPrimaryFont(computed.fontFamily);
|
|
1264
|
+
if (primaryFont) {
|
|
1265
|
+
result.fontFamily = primaryFont;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
// Line-height: Use computed style if not already set from CSS parsing
|
|
1269
|
+
if (!result.lineHeight && computed.lineHeight) {
|
|
1270
|
+
// Convert computed lineHeight (e.g., "27.2px") to a ratio or keep as-is
|
|
1271
|
+
result.lineHeight = computed.lineHeight;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
catch {
|
|
1275
|
+
// getComputedStyle may fail in some environments
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// Resolve CSS variables in all collected style values
|
|
1279
|
+
// This handles inline styles like "background-color: var(--color-bg)"
|
|
1280
|
+
// and CSS class styles that reference variables
|
|
1281
|
+
const resolveVar = (val) => {
|
|
1282
|
+
if (!val || !val.includes("var("))
|
|
1283
|
+
return val;
|
|
1284
|
+
const varMatch = val.match(/var\s*\(\s*(--[a-zA-Z0-9-]+)\s*(?:,\s*([^)]+))?\s*\)/);
|
|
1285
|
+
if (varMatch) {
|
|
1286
|
+
const resolved = cssContext.variables.get(varMatch[1]);
|
|
1287
|
+
if (resolved)
|
|
1288
|
+
return resolved;
|
|
1289
|
+
// Use fallback value if provided
|
|
1290
|
+
if (varMatch[2])
|
|
1291
|
+
return varMatch[2].trim();
|
|
1292
|
+
}
|
|
1293
|
+
return val;
|
|
1294
|
+
};
|
|
1295
|
+
result.backgroundColor = resolveVar(result.backgroundColor);
|
|
1296
|
+
result.color = resolveVar(result.color);
|
|
1297
|
+
result.borderColor = resolveVar(result.borderColor);
|
|
1298
|
+
result.border = resolveVar(result.border);
|
|
1299
|
+
result.borderLeft = resolveVar(result.borderLeft);
|
|
1300
|
+
result.borderBottom = resolveVar(result.borderBottom);
|
|
1301
|
+
result.borderTop = resolveVar(result.borderTop);
|
|
1302
|
+
// When a gradient text fill is present (background-clip: text), the gradient's first stop
|
|
1303
|
+
// color should be used as the text color fallback. This overrides any inherited or
|
|
1304
|
+
// element-level color (e.g. h1 { color: #1a1a1a }) because the gradient IS the intended
|
|
1305
|
+
// text color — the regular CSS color would only be visible without -webkit-background-clip: text.
|
|
1306
|
+
if (result.gradient && result.gradient.stops.length > 0) {
|
|
1307
|
+
result.color = result.gradient.stops[0].color;
|
|
1308
|
+
}
|
|
1309
|
+
return result;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Get color for an element by checking its classes against parsed CSS rules.
|
|
1313
|
+
*/
|
|
1314
|
+
export function getColorFromClasses(element, cssContext) {
|
|
1315
|
+
const classAttr = element.getAttribute("class");
|
|
1316
|
+
if (!classAttr)
|
|
1317
|
+
return undefined;
|
|
1318
|
+
const classes = classAttr.split(/\s+/).filter(c => c.length > 0);
|
|
1319
|
+
// First, check compound selectors (higher specificity)
|
|
1320
|
+
// Look for compound keys in classColors where the element has ALL required classes
|
|
1321
|
+
if (classes.length >= 2) {
|
|
1322
|
+
// Check all compound keys in classColors
|
|
1323
|
+
for (const [key, color] of cssContext.classColors) {
|
|
1324
|
+
if (key.includes('.')) {
|
|
1325
|
+
// This is a compound key (e.g., "metric-change.positive")
|
|
1326
|
+
const requiredClasses = key.split('.');
|
|
1327
|
+
if (requiredClasses.every(c => classes.includes(c))) {
|
|
1328
|
+
return extractHexColor(color);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
// Then check simple class selectors
|
|
1334
|
+
for (const className of classes) {
|
|
1335
|
+
const color = cssContext.classColors.get(className);
|
|
1336
|
+
if (color) {
|
|
1337
|
+
return extractHexColor(color);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return undefined;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Extract the text color from an element's inline style, CSS class rules, or computed style.
|
|
1344
|
+
* Checks inline style first, then CSS class rules, then element type rules, then computed style if available.
|
|
1345
|
+
* Returns the actual color from the HTML - does NOT filter out any colors.
|
|
1346
|
+
* Per the skill rules: ALL styling values MUST be extracted from HTML, never filtered.
|
|
1347
|
+
*/
|
|
1348
|
+
export function extractTextColor(element, cssContext) {
|
|
1349
|
+
// Check inline style first (highest priority)
|
|
1350
|
+
const inlineStyle = element.getAttribute("style") || "";
|
|
1351
|
+
const colorMatch = inlineStyle.match(/(?:^|;)\s*color:\s*([^;]+)/i);
|
|
1352
|
+
if (colorMatch) {
|
|
1353
|
+
let colorValue = colorMatch[1].trim();
|
|
1354
|
+
// Resolve CSS variables in inline styles (e.g., var(--color-muted))
|
|
1355
|
+
if (cssContext && colorValue.includes("var(")) {
|
|
1356
|
+
const varMatch = colorValue.match(/var\s*\(\s*(--[a-zA-Z0-9-]+)\s*\)/);
|
|
1357
|
+
if (varMatch) {
|
|
1358
|
+
const resolvedValue = cssContext.variables.get(varMatch[1]);
|
|
1359
|
+
if (resolvedValue) {
|
|
1360
|
+
colorValue = resolvedValue;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const color = extractHexColor(colorValue);
|
|
1365
|
+
if (color) {
|
|
1366
|
+
return color;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// Check CSS rules using getElementStyles which handles the full cascade:
|
|
1370
|
+
// element type < class < compound class < nested selectors < inline styles
|
|
1371
|
+
if (cssContext) {
|
|
1372
|
+
const elementStyles = getElementStyles(element, cssContext);
|
|
1373
|
+
if (elementStyles.color) {
|
|
1374
|
+
const color = extractHexColor(elementStyles.color);
|
|
1375
|
+
if (color) {
|
|
1376
|
+
return color;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
// Fallback to classColors map for colors not captured by getElementStyles
|
|
1380
|
+
const classColor = getColorFromClasses(element, cssContext);
|
|
1381
|
+
if (classColor) {
|
|
1382
|
+
return classColor;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
// Try computed style if available (browser environment)
|
|
1386
|
+
if (typeof window !== "undefined" && window.getComputedStyle) {
|
|
1387
|
+
try {
|
|
1388
|
+
const computed = window.getComputedStyle(element);
|
|
1389
|
+
const computedColor = computed.color;
|
|
1390
|
+
if (computedColor) {
|
|
1391
|
+
const color = extractHexColor(computedColor);
|
|
1392
|
+
if (color) {
|
|
1393
|
+
return color;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
catch {
|
|
1398
|
+
// getComputedStyle may fail in some environments
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return undefined;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Extract border color from a border shorthand or border-color property.
|
|
1405
|
+
* Also handles borderLeft, borderRight, borderTop, borderBottom.
|
|
1406
|
+
*/
|
|
1407
|
+
export function extractBorderColorFromStyle(style) {
|
|
1408
|
+
if (style.borderColor) {
|
|
1409
|
+
return extractHexColor(style.borderColor);
|
|
1410
|
+
}
|
|
1411
|
+
// Check border shorthand
|
|
1412
|
+
if (style.border) {
|
|
1413
|
+
// Parse "1px solid #e5e7eb" or "1px solid rgb(184, 218, 255)" or "1px solid blue"
|
|
1414
|
+
// Try hex color first (most common)
|
|
1415
|
+
const hexMatch = style.border.match(/#([0-9a-fA-F]{3,6})/);
|
|
1416
|
+
if (hexMatch) {
|
|
1417
|
+
let hex = hexMatch[1];
|
|
1418
|
+
if (hex.length === 3) {
|
|
1419
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1420
|
+
}
|
|
1421
|
+
return hex.toUpperCase();
|
|
1422
|
+
}
|
|
1423
|
+
// Try rgb/rgba color
|
|
1424
|
+
const rgbMatch = style.border.match(/(rgba?\s*\([^)]+\))/i);
|
|
1425
|
+
if (rgbMatch) {
|
|
1426
|
+
return extractHexColor(rgbMatch[1]);
|
|
1427
|
+
}
|
|
1428
|
+
// Try named color (last word in the shorthand, after "solid" or "dashed" etc.)
|
|
1429
|
+
const parts = style.border.trim().split(/\s+/);
|
|
1430
|
+
const lastPart = parts[parts.length - 1];
|
|
1431
|
+
if (lastPart && !lastPart.match(/^\d/) && !["solid", "dashed", "dotted", "double", "none", "hidden", "groove", "ridge", "inset", "outset"].includes(lastPart)) {
|
|
1432
|
+
return extractHexColor(lastPart);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
// Check individual border properties (borderLeft, borderRight, borderTop, borderBottom)
|
|
1436
|
+
const borderProps = [style.borderLeft, style.borderRight, style.borderTop, style.borderBottom];
|
|
1437
|
+
for (const borderValue of borderProps) {
|
|
1438
|
+
if (borderValue) {
|
|
1439
|
+
// Parse "4px solid #7c3aed" or similar — try hex first, then rgb, then named
|
|
1440
|
+
const hexMatch = borderValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
1441
|
+
if (hexMatch) {
|
|
1442
|
+
let hex = hexMatch[1];
|
|
1443
|
+
if (hex.length === 3) {
|
|
1444
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1445
|
+
}
|
|
1446
|
+
return hex.toUpperCase();
|
|
1447
|
+
}
|
|
1448
|
+
const rgbMatch = borderValue.match(/(rgba?\s*\([^)]+\))/i);
|
|
1449
|
+
if (rgbMatch) {
|
|
1450
|
+
return extractHexColor(rgbMatch[1]);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return undefined;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Check if an element has a border-bottom style that should become a horizontal rule.
|
|
1458
|
+
* Returns the border color if found, undefined otherwise.
|
|
1459
|
+
*/
|
|
1460
|
+
export function extractBorderBottomColor(element, cssContext) {
|
|
1461
|
+
// Check inline style for border-bottom
|
|
1462
|
+
const inlineStyle = element.getAttribute("style") || "";
|
|
1463
|
+
const borderMatch = inlineStyle.match(/border-bottom:\s*([^;]+)/i);
|
|
1464
|
+
if (borderMatch) {
|
|
1465
|
+
const borderValue = borderMatch[1];
|
|
1466
|
+
// Parse "1px solid #e5e7eb" or similar
|
|
1467
|
+
const colorMatch = borderValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
1468
|
+
if (colorMatch) {
|
|
1469
|
+
let hex = colorMatch[1];
|
|
1470
|
+
if (hex.length === 3) {
|
|
1471
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1472
|
+
}
|
|
1473
|
+
return hex.toUpperCase();
|
|
1474
|
+
}
|
|
1475
|
+
// Check for CSS variable in border
|
|
1476
|
+
const varMatch = borderValue.match(/var\s*\(\s*(--[a-zA-Z0-9-]+)\s*\)/);
|
|
1477
|
+
if (varMatch) {
|
|
1478
|
+
const varValue = cssContext.variables.get(varMatch[1]);
|
|
1479
|
+
if (varValue) {
|
|
1480
|
+
const hex = extractHexColor(varValue);
|
|
1481
|
+
if (hex)
|
|
1482
|
+
return hex;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
// Check CSS styles for border-bottom (generalized approach)
|
|
1487
|
+
const styles = getElementStyles(element, cssContext);
|
|
1488
|
+
// Check border-bottom first (most specific for horizontal rules)
|
|
1489
|
+
if (styles.borderBottom) {
|
|
1490
|
+
const borderValue = styles.borderBottom;
|
|
1491
|
+
const colorMatch = borderValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
1492
|
+
if (colorMatch) {
|
|
1493
|
+
let hex = colorMatch[1];
|
|
1494
|
+
if (hex.length === 3) {
|
|
1495
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1496
|
+
}
|
|
1497
|
+
return hex.toUpperCase();
|
|
1498
|
+
}
|
|
1499
|
+
// Check for CSS variable reference in border-bottom
|
|
1500
|
+
const varMatch = borderValue.match(/var\s*\(\s*(--[a-zA-Z0-9-]+)\s*\)/);
|
|
1501
|
+
if (varMatch) {
|
|
1502
|
+
const varValue = cssContext.variables.get(varMatch[1]);
|
|
1503
|
+
if (varValue) {
|
|
1504
|
+
const hex = extractHexColor(varValue);
|
|
1505
|
+
if (hex)
|
|
1506
|
+
return hex;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
// Check if this element has a border-bottom defined in CSS
|
|
1511
|
+
// The border property might be a shorthand like "1px solid #e5e7eb"
|
|
1512
|
+
if (styles.border) {
|
|
1513
|
+
const borderValue = styles.border;
|
|
1514
|
+
const colorMatch = borderValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
1515
|
+
if (colorMatch) {
|
|
1516
|
+
let hex = colorMatch[1];
|
|
1517
|
+
if (hex.length === 3) {
|
|
1518
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1519
|
+
}
|
|
1520
|
+
return hex.toUpperCase();
|
|
1521
|
+
}
|
|
1522
|
+
// Check for CSS variable reference
|
|
1523
|
+
const varMatch = borderValue.match(/var\s*\(\s*(--[a-zA-Z0-9-]+)\s*\)/);
|
|
1524
|
+
if (varMatch) {
|
|
1525
|
+
const varValue = cssContext.variables.get(varMatch[1]);
|
|
1526
|
+
if (varValue) {
|
|
1527
|
+
const hex = extractHexColor(varValue);
|
|
1528
|
+
if (hex)
|
|
1529
|
+
return hex;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
// Also check borderColor directly
|
|
1534
|
+
if (styles.borderColor) {
|
|
1535
|
+
const hex = extractHexColor(styles.borderColor);
|
|
1536
|
+
if (hex)
|
|
1537
|
+
return hex;
|
|
1538
|
+
}
|
|
1539
|
+
return undefined;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Extract color from a border-top CSS property on an element.
|
|
1543
|
+
* Checks inline styles and CSS classes, and resolves CSS variables.
|
|
1544
|
+
*/
|
|
1545
|
+
export function extractBorderTopColor(element, cssContext) {
|
|
1546
|
+
// First, check inline style for border-top (highest specificity)
|
|
1547
|
+
const inlineStyle = element.getAttribute("style") || "";
|
|
1548
|
+
const borderMatch = inlineStyle.match(/border-top:\s*([^;]+)/i);
|
|
1549
|
+
if (borderMatch) {
|
|
1550
|
+
const borderValue = borderMatch[1];
|
|
1551
|
+
// Parse "1px solid #e5e7eb" or similar
|
|
1552
|
+
const colorMatch = borderValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
1553
|
+
if (colorMatch) {
|
|
1554
|
+
let hex = colorMatch[1];
|
|
1555
|
+
if (hex.length === 3) {
|
|
1556
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1557
|
+
}
|
|
1558
|
+
return hex.toUpperCase();
|
|
1559
|
+
}
|
|
1560
|
+
// Check for CSS variable in border
|
|
1561
|
+
const varMatch = borderValue.match(/var\s*\(\s*(--[a-zA-Z0-9-]+)\s*\)/);
|
|
1562
|
+
if (varMatch) {
|
|
1563
|
+
const varValue = cssContext.variables.get(varMatch[1]);
|
|
1564
|
+
if (varValue) {
|
|
1565
|
+
const hex = extractHexColor(varValue);
|
|
1566
|
+
if (hex)
|
|
1567
|
+
return hex;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
// Then, check CSS class styles (e.g., .menu-footer { border-top: 2px solid #b8860b; })
|
|
1572
|
+
const styles = getElementStyles(element, cssContext);
|
|
1573
|
+
if (styles.borderTop) {
|
|
1574
|
+
const borderValue = styles.borderTop;
|
|
1575
|
+
// Parse "2px solid #b8860b" or similar
|
|
1576
|
+
const colorMatch = borderValue.match(/#([0-9a-fA-F]{3,6})/);
|
|
1577
|
+
if (colorMatch) {
|
|
1578
|
+
let hex = colorMatch[1];
|
|
1579
|
+
if (hex.length === 3) {
|
|
1580
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1581
|
+
}
|
|
1582
|
+
return hex.toUpperCase();
|
|
1583
|
+
}
|
|
1584
|
+
// Check for CSS variable in border that was already resolved
|
|
1585
|
+
// (resolveValue in parseCssContext should have resolved it)
|
|
1586
|
+
const resolved = extractHexColor(borderValue);
|
|
1587
|
+
if (resolved)
|
|
1588
|
+
return resolved;
|
|
1589
|
+
}
|
|
1590
|
+
return undefined;
|
|
1591
|
+
}
|
|
1592
|
+
//# sourceMappingURL=parse-css.js.map
|