mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
"dev": "bun run server/index.js",
|
||||
"preview": "bun run build && wrangler pages dev",
|
||||
"deploy": "bun run build && wrangler pages deploy build/",
|
||||
"test": "bun test tests/build.test.js tests/detect-antipatterns.test.js && node --test tests/detect-antipatterns-fixtures.test.mjs && node --test tests/detect-antipatterns-browser.test.mjs && node --test tests/cleanup-deprecated.test.mjs && node --test tests/live-wrap.test.mjs && node --test tests/live-server.test.mjs && node --test tests/framework-fixtures.test.mjs",
|
||||
"test": "bun test tests/build.test.js tests/detect-antipatterns.test.js && node --test tests/detect-antipatterns-fixtures.test.mjs && node --test tests/detect-antipatterns-browser.test.mjs && node --test tests/cleanup-deprecated.test.mjs && node --test tests/live-wrap.test.mjs && node --test tests/live-accept.test.mjs && node --test tests/live-server.test.mjs && node --test tests/framework-fixtures.test.mjs",
|
||||
"prepack": "cp README.md README.repo.md && cp README.npm.md README.md",
|
||||
"postpack": "cp README.repo.md README.md && rm README.repo.md",
|
||||
"screenshot": "bun run scripts/screenshot-antipatterns.js",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Tests for live-accept.mjs — the deterministic accept/discard helper.
|
||||
* Run with: node --test tests/live-accept.test.mjs
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ACCEPT = resolve(__dirname, '..', 'source/skills/impeccable/scripts/live-accept.mjs');
|
||||
|
||||
function runAccept(cwd, args) {
|
||||
try {
|
||||
const out = execFileSync('node', [ACCEPT, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
return JSON.parse(out.trim());
|
||||
} catch (err) {
|
||||
const body = err.stdout?.toString().trim() || err.stderr?.toString().trim() || '';
|
||||
return JSON.parse(body || '{}');
|
||||
}
|
||||
}
|
||||
|
||||
describe('live-accept — style-element edge cases', () => {
|
||||
let tmp;
|
||||
beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'impeccable-accept-test-')); });
|
||||
afterEach(() => { rmSync(tmp, { recursive: true, force: true }); });
|
||||
|
||||
// Historical bug: extractVariant flipped into "inStyle" mode on <style and
|
||||
// scanned for </style> line-by-line. JSX self-closing <style ... /> has no
|
||||
// separate closer, so it got stuck forever and missed data-impeccable-variant
|
||||
// divs that came after.
|
||||
it('finds the accepted variant after a JSX self-closing <style /> block', () => {
|
||||
const html = `<body>
|
||||
<!-- impeccable-variants-start SELFC -->
|
||||
<div data-impeccable-variants="SELFC" data-impeccable-variant-count="3" style="display: contents">
|
||||
<div data-impeccable-variant="original">
|
||||
<p class="hook">original text</p>
|
||||
</div>
|
||||
<style data-impeccable-css="SELFC" dangerouslySetInnerHTML={{ __html: '@scope ([data-impeccable-variant="1"]) { .hook { color: red; } }' }} />
|
||||
<div data-impeccable-variant="1">
|
||||
<p class="hook">variant one</p>
|
||||
</div>
|
||||
<div data-impeccable-variant="2" style="display: none">
|
||||
<p class="hook">variant two</p>
|
||||
</div>
|
||||
<div data-impeccable-variant="3" style="display: none">
|
||||
<p class="hook">variant three</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end SELFC -->
|
||||
</body>`;
|
||||
writeFileSync(join(tmp, 'page.html'), html);
|
||||
|
||||
const result = runAccept(tmp, ['--id', 'SELFC', '--variant', '2']);
|
||||
assert.equal(result.handled, true, `accept should succeed: ${JSON.stringify(result)}`);
|
||||
|
||||
const after = readFileSync(join(tmp, 'page.html'), 'utf-8');
|
||||
// Self-closing style has no extractable CSS body, so there's nothing to carbonize —
|
||||
// no carbonize block, no data-impeccable-variant wrapper (it would serve no purpose).
|
||||
assert.ok(!after.includes('impeccable-carbonize-start'), 'no carbonize block (self-closing style has no body)');
|
||||
assert.ok(!after.includes('impeccable-variants-start'), 'variant markers removed');
|
||||
assert.ok(after.includes('variant two'), 'variant 2 content kept');
|
||||
assert.ok(!after.includes('variant three'), 'other variant content dropped');
|
||||
assert.ok(!after.includes('variant one'), 'other variant content dropped');
|
||||
assert.ok(!after.includes('original text'), 'original content dropped');
|
||||
});
|
||||
|
||||
// Variant: same-line <style>…</style> block should also be treated as a
|
||||
// single skipped unit; the line has both open and close tags.
|
||||
it('finds the accepted variant after a single-line <style>…</style> block', () => {
|
||||
const html = `<body>
|
||||
<!-- impeccable-variants-start ONELINE -->
|
||||
<div data-impeccable-variants="ONELINE" data-impeccable-variant-count="3" style="display: contents">
|
||||
<div data-impeccable-variant="original"><p class="hook">original</p></div>
|
||||
<style data-impeccable-css="ONELINE">@scope ([data-impeccable-variant="1"]) { .hook { color: red; } }</style>
|
||||
<div data-impeccable-variant="1"><p class="hook">variant one</p></div>
|
||||
<div data-impeccable-variant="2" style="display: none"><p class="hook">variant two</p></div>
|
||||
<div data-impeccable-variant="3" style="display: none"><p class="hook">variant three</p></div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end ONELINE -->
|
||||
</body>`;
|
||||
writeFileSync(join(tmp, 'page.html'), html);
|
||||
|
||||
const result = runAccept(tmp, ['--id', 'ONELINE', '--variant', '3']);
|
||||
assert.equal(result.handled, true, `accept should succeed: ${JSON.stringify(result)}`);
|
||||
|
||||
const after = readFileSync(join(tmp, 'page.html'), 'utf-8');
|
||||
assert.ok(after.includes('data-impeccable-variant="3"'), 'accepted wrapper for variant 3 present');
|
||||
assert.ok(after.includes('variant three'), 'variant 3 content kept');
|
||||
assert.ok(!after.includes('variant two'), 'other variant content dropped');
|
||||
});
|
||||
|
||||
// Baseline: the standard multi-line <style>...</style> case must keep working.
|
||||
it('finds the accepted variant after a multi-line <style>…</style> block (regression baseline)', () => {
|
||||
const html = `<body>
|
||||
<!-- impeccable-variants-start MULTI -->
|
||||
<div data-impeccable-variants="MULTI" data-impeccable-variant-count="3" style="display: contents">
|
||||
<div data-impeccable-variant="original"><p class="hook">original</p></div>
|
||||
<style data-impeccable-css="MULTI">
|
||||
@scope ([data-impeccable-variant="1"]) { .hook { color: red; } }
|
||||
@scope ([data-impeccable-variant="2"]) { .hook { color: green; } }
|
||||
</style>
|
||||
<div data-impeccable-variant="1"><p class="hook">variant one</p></div>
|
||||
<div data-impeccable-variant="2" style="display: none"><p class="hook">variant two</p></div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end MULTI -->
|
||||
</body>`;
|
||||
writeFileSync(join(tmp, 'page.html'), html);
|
||||
|
||||
const result = runAccept(tmp, ['--id', 'MULTI', '--variant', '1']);
|
||||
assert.equal(result.handled, true, `accept should succeed: ${JSON.stringify(result)}`);
|
||||
|
||||
const after = readFileSync(join(tmp, 'page.html'), 'utf-8');
|
||||
assert.ok(after.includes('data-impeccable-variant="1"'), 'accepted wrapper for variant 1 present');
|
||||
assert.ok(after.includes('variant one'), 'variant 1 content kept');
|
||||
});
|
||||
|
||||
// Discard must restore the original element after a self-closing <style />,
|
||||
// proving extractOriginal also survives the style pattern.
|
||||
it('discard restores the original element after a JSX self-closing <style />', () => {
|
||||
const html = `<body>
|
||||
<!-- impeccable-variants-start DISC -->
|
||||
<div data-impeccable-variants="DISC" data-impeccable-variant-count="2" style="display: contents">
|
||||
<div data-impeccable-variant="original"><p class="hook">ORIGINAL CONTENT</p></div>
|
||||
<style data-impeccable-css="DISC" dangerouslySetInnerHTML={{ __html: '@scope ([data-impeccable-variant="1"]) { .hook { color: red; } }' }} />
|
||||
<div data-impeccable-variant="1"><p class="hook">variant one</p></div>
|
||||
<div data-impeccable-variant="2" style="display: none"><p class="hook">variant two</p></div>
|
||||
</div>
|
||||
<!-- impeccable-variants-end DISC -->
|
||||
</body>`;
|
||||
writeFileSync(join(tmp, 'page.html'), html);
|
||||
|
||||
const result = runAccept(tmp, ['--id', 'DISC', '--discard']);
|
||||
assert.equal(result.handled, true, `discard should succeed: ${JSON.stringify(result)}`);
|
||||
|
||||
const after = readFileSync(join(tmp, 'page.html'), 'utf-8');
|
||||
assert.ok(after.includes('ORIGINAL CONTENT'), 'original restored');
|
||||
assert.ok(!after.includes('impeccable-variants-start'), 'wrapper markers gone');
|
||||
assert.ok(!after.includes('variant one'), 'variants dropped');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user