mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
fix(live-accept): handle JSX self-closing <style />, single-line variants, and same-line style blocks
Three related extraction bugs surfaced in the EAC session all rooted in the line-based state machine: 1. `<style ... />` (JSX self-closing) had no separate `</style>` for the "skip until close" mode to exit on, so the state machine stuck and every `data-impeccable-variant` marker after it got missed. Accept reported `handled: false, error: "Variant N not found"`. 2. A variant whose entire `<div ...>...</div>` sits on one line had its body silently discarded — the marker line was `continue`d past, and the extractor started capturing from the next line, which usually belonged to a different variant or the wrapper close. 3. `extractCss` kept scanning for `</style>` after a self-closing opener, greedily swallowing every subsequent variant div as "CSS". Result: a mangled carbonize block stuffed with HTML and a duplicate variant rendered below. ## Fix Replaced the line-based state machine with a string-based flow: - `stripStyleAndJoin(lines, block)` returns the wrapper text with `<style>` elements fully removed. Handles self-closing, same-line open+close, and multi-line open/close. Markers inside CSS strings (e.g. `@scope ([data-impeccable-variant="1"])`) are gone by the time extraction runs — no false positives. - `extractInnerByAttr(text, attrMatch)` is a balanced-tag matcher that walks the joined text finding `<TAG ...attrMatch...>…</TAG>` with proper depth tracking for nested same-tag elements. Handles single-line, multi-line, and deeply nested variants. - `extractOriginal` and `extractVariant` are thin wrappers over the above. - `extractCss` gets explicit same-line handling: returns null for self-closing (nothing to carbonize), extracts inner content via regex for same-line `<style>…</style>`, falls through to the existing multi-line path otherwise. ## Tests New tests/live-accept.test.mjs with four cases — all failing before, all passing after: - Self-closing `<style />` with dangerouslySetInnerHTML - Single-line `<style>…</style>` - Multi-line `<style>...</style>` (regression baseline) - Discard restores the original element after self-closing style Wired into `bun run test`. Full suite passes. Credit: precise repro + root-cause trace from the other agent in the EAC session. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a4832adf2f
commit
cd8dbff014
@@ -51,6 +51,16 @@ Editorial has permission for Committed, Full palette, and Drenched strategies. U
|
||||
- Don't center everything. Left-aligned in asymmetric compositions feels more designed.
|
||||
- When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` — breakpoint-free responsiveness.
|
||||
|
||||
## Imagery
|
||||
|
||||
Editorial register leans on imagery. A restaurant, hotel, magazine, or product landing page without any imagery reads as incomplete, not as restrained. A solid-color rectangle where a hero image should go is worse than a representative stock photo.
|
||||
|
||||
- **For greenfield work without local assets, reach for stock imagery** from Unsplash (`https://images.unsplash.com/photo-{id}?w=…&q=80`), Pexels, or similar. A well-chosen Unsplash photo is a valid deliverable — colored placeholder blocks are not.
|
||||
- **Search for the brand's physical object**, not the generic category: "handmade pasta on a scratched wooden table" beats "Italian food"; "cypress trees above a limestone hotel facade at dusk" beats "luxury hotel".
|
||||
- **One decisive photo beats five mediocre ones.** Hero imagery should commit to a mood; padding with more stock doesn't rescue an indecisive one.
|
||||
- **Don't stop at zero** when the brief implies imagery. A moto forum without motorcycle photos, a restaurant without food, a hotel without a view — these read as stubs, not as editorial restraint.
|
||||
- **Alt text is part of the voice.** "Coastal fettuccine, hand-cut, served on the terrace" beats "pasta dish".
|
||||
|
||||
## Motion
|
||||
|
||||
- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions.
|
||||
|
||||
@@ -192,91 +192,117 @@ function findMarkerBlock(id, lines) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the original element content from within the variant wrapper.
|
||||
* Returns an array of lines (still indented as stored in the wrapper).
|
||||
*
|
||||
* CSS inside a <style> block can reference `data-impeccable-variant="N"` via
|
||||
* `@scope`, which would falsely match the HTML div we're looking for — so skip
|
||||
* style regions entirely.
|
||||
* Join wrapper lines into a single string with `<style>` elements removed so
|
||||
* marker matching and div-depth tracking aren't confused by:
|
||||
* - CSS `@scope ([data-impeccable-variant="N"])` strings that look like the
|
||||
* HTML marker we're searching for
|
||||
* - JSX self-closing `<style ... />` (no separate `</style>` to close on)
|
||||
* - Same-line `<style>…</style>` blocks
|
||||
* - Multi-line `<style>\n…\n</style>` blocks
|
||||
*/
|
||||
function extractOriginal(lines, block) {
|
||||
let inOriginal = false;
|
||||
function stripStyleAndJoin(lines, block) {
|
||||
const out = [];
|
||||
let inStyle = false;
|
||||
let depth = 0;
|
||||
const content = [];
|
||||
|
||||
for (let i = block.start; i <= block.end; i++) {
|
||||
const line = lines[i];
|
||||
let line = lines[i];
|
||||
|
||||
if (!inStyle && /<style[\s>]/.test(line)) { inStyle = true; continue; }
|
||||
if (inStyle) {
|
||||
if (line.trimStart().startsWith('</style>')) inStyle = false;
|
||||
continue;
|
||||
}
|
||||
if (!inStyle) {
|
||||
// Strip any complete <style> elements on this line (self-closed or
|
||||
// same-line-closed), including their body content.
|
||||
line = line
|
||||
.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/g, '')
|
||||
.replace(/<style\b[^>]*\/\s*>/g, '');
|
||||
|
||||
if (!inOriginal && line.includes('data-impeccable-variant="original"')) {
|
||||
inOriginal = true;
|
||||
depth = 1;
|
||||
continue; // skip the opening <div data-impeccable-variant="original">
|
||||
}
|
||||
|
||||
if (inOriginal) {
|
||||
// Count div opens/closes to find the matching </div>
|
||||
const opens = (line.match(/<div[\s>]/g) || []).length;
|
||||
const closes = (line.match(/<\/div\s*>/g) || []).length;
|
||||
depth += opens - closes;
|
||||
|
||||
if (depth <= 0) break; // this is the closing </div> of the original wrapper
|
||||
content.push(line);
|
||||
// If a <style> opener remains (multi-line body starts here), strip from
|
||||
// the opener to end-of-line and flip into skip mode.
|
||||
const openerIdx = line.search(/<style\b/);
|
||||
if (openerIdx !== -1) {
|
||||
line = line.slice(0, openerIdx);
|
||||
inStyle = true;
|
||||
}
|
||||
out.push(line);
|
||||
} else {
|
||||
// In multi-line style body; drop everything until we see </style>.
|
||||
const closeIdx = line.search(/<\/style\s*>/);
|
||||
if (closeIdx !== -1) {
|
||||
inStyle = false;
|
||||
out.push(line.slice(closeIdx).replace(/<\/style\s*>/, ''));
|
||||
}
|
||||
// else: skip line entirely
|
||||
}
|
||||
}
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
return content;
|
||||
/**
|
||||
* Find the inner content of `<TAG ...attrMatch...>…</TAG>` inside `text`,
|
||||
* handling nested same-tag elements via depth counting. `attrMatch` is a
|
||||
* regex source fragment that must appear inside the opener tag.
|
||||
* Returns the inner string (may be empty), or null if not found.
|
||||
*/
|
||||
function extractInnerByAttr(text, attrMatch) {
|
||||
const openerRe = new RegExp('<([A-Za-z][A-Za-z0-9]*)\\b[^>]*' + attrMatch + '[^>]*>');
|
||||
const openMatch = text.match(openerRe);
|
||||
if (!openMatch) return null;
|
||||
|
||||
const tagName = openMatch[1];
|
||||
const innerStart = openMatch.index + openMatch[0].length;
|
||||
|
||||
// Match any opener or closer of this tag name after innerStart.
|
||||
// (Does not match self-closing <TAG … />, which doesn't contribute to depth.)
|
||||
const tagRe = new RegExp('<(?:/)?' + tagName + '\\b[^>]*>', 'g');
|
||||
tagRe.lastIndex = innerStart;
|
||||
|
||||
let depth = 1;
|
||||
let m;
|
||||
while ((m = tagRe.exec(text))) {
|
||||
const isClose = m[0].startsWith('</');
|
||||
const isSelfClose = !isClose && /\/\s*>$/.test(m[0]);
|
||||
if (isClose) {
|
||||
depth--;
|
||||
if (depth === 0) return text.slice(innerStart, m.index);
|
||||
} else if (!isSelfClose) {
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the original element content from within the variant wrapper.
|
||||
* Returns an array of lines.
|
||||
*/
|
||||
function extractOriginal(lines, block) {
|
||||
const text = stripStyleAndJoin(lines, block);
|
||||
const inner = extractInnerByAttr(text, 'data-impeccable-variant="original"');
|
||||
if (inner === null) return [];
|
||||
return inner.split('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a specific variant's inner content (stripping the wrapper div).
|
||||
* Returns an array of lines, or null if not found.
|
||||
*
|
||||
* Skip <style> blocks — see extractOriginal for why.
|
||||
*/
|
||||
function extractVariant(lines, block, variantNum) {
|
||||
let inVariant = false;
|
||||
let inStyle = false;
|
||||
let depth = 0;
|
||||
const content = [];
|
||||
|
||||
for (let i = block.start; i <= block.end; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (!inStyle && /<style[\s>]/.test(line)) { inStyle = true; continue; }
|
||||
if (inStyle) {
|
||||
if (line.trimStart().startsWith('</style>')) inStyle = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inVariant && line.includes('data-impeccable-variant="' + variantNum + '"')) {
|
||||
inVariant = true;
|
||||
depth = 1;
|
||||
continue; // skip the opening <div data-impeccable-variant="N">
|
||||
}
|
||||
|
||||
if (inVariant) {
|
||||
const opens = (line.match(/<div[\s>]/g) || []).length;
|
||||
const closes = (line.match(/<\/div\s*>/g) || []).length;
|
||||
depth += opens - closes;
|
||||
|
||||
if (depth <= 0) break; // closing </div> of the variant wrapper
|
||||
content.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return content.length > 0 ? content : null;
|
||||
const text = stripStyleAndJoin(lines, block);
|
||||
const inner = extractInnerByAttr(text, 'data-impeccable-variant="' + variantNum + '"');
|
||||
if (inner === null) return null;
|
||||
const result = inner.split('\n');
|
||||
// Collapse a lone empty leading/trailing line (common after string splice).
|
||||
while (result.length > 1 && result[0].trim() === '') result.shift();
|
||||
while (result.length > 1 && result[result.length - 1].trim() === '') result.pop();
|
||||
return result.length > 0 ? result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the colocated <style> block content (between the style tags).
|
||||
* Returns an array of CSS lines, or null if no style block found.
|
||||
*
|
||||
* Handles three shapes of `<style data-impeccable-css="ID" ...>`:
|
||||
* 1. Self-closing: `<style ... />` — no body; return null (nothing to carbonize).
|
||||
* 2. Same-line open+close: `<style>...</style>` — return the inner content.
|
||||
* 3. Multi-line: `<style>` on one line, `</style>` on a later line — return
|
||||
* the lines between them.
|
||||
*/
|
||||
function extractCss(lines, block, id) {
|
||||
const styleAttr = 'data-impeccable-css="' + id + '"';
|
||||
@@ -287,6 +313,14 @@ function extractCss(lines, block, id) {
|
||||
const line = lines[i];
|
||||
|
||||
if (!inStyle && line.includes(styleAttr)) {
|
||||
// Self-closing: nothing to carbonize.
|
||||
if (/<style\b[^>]*\/\s*>/.test(line)) return null;
|
||||
// Same-line open + close: extract inner text.
|
||||
const sameLine = line.match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/);
|
||||
if (sameLine) {
|
||||
const inner = sameLine[1];
|
||||
return inner.length > 0 ? inner.split('\n') : null;
|
||||
}
|
||||
inStyle = true;
|
||||
continue; // skip the <style> opening tag
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user