mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
🐛 Fix DESIGN.md Layout and Shapes parsing (#481)
* 🐛 Fix DESIGN.md Layout and Shapes parsing Prepared with AI assistance. * ♻️ Refine canonical design parser coverage Prepared with AI assistance.
This commit is contained in:
@@ -2,15 +2,20 @@
|
||||
// the live-mode design-system panel can render. Deterministic, dependency-free.
|
||||
//
|
||||
// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body
|
||||
// (prose with six canonical H2 sections). When frontmatter is present, it's
|
||||
// (prose with eight canonical H2 sections). When frontmatter is present, it's
|
||||
// exposed on `model.frontmatter` alongside the prose-scraped sections;
|
||||
// consumers can prefer frontmatter values and fall back to prose.
|
||||
|
||||
// Array order is also match precedence: matchCanonicalSection's keyword-contained
|
||||
// pass returns the first entry a heading contains, so reordering this changes
|
||||
// which section an ambiguous heading resolves to.
|
||||
const CANONICAL_SECTIONS = [
|
||||
'Overview',
|
||||
'Colors',
|
||||
'Typography',
|
||||
'Layout',
|
||||
'Elevation',
|
||||
'Shapes',
|
||||
'Components',
|
||||
"Do's and Don'ts",
|
||||
];
|
||||
@@ -662,11 +667,19 @@ function parseTypeBullet(bullet) {
|
||||
};
|
||||
}
|
||||
|
||||
function extractElevation(section) {
|
||||
function extractGuidance(section) {
|
||||
if (!section) return null;
|
||||
const subs = splitSubsections(section.lines);
|
||||
return {
|
||||
subtitle: section.subtitle,
|
||||
description: collectParagraphs(subs[0].lines).join(' ') || null,
|
||||
rules: extractNamedRules(section.lines),
|
||||
};
|
||||
}
|
||||
|
||||
const description = collectParagraphs(subs[0].lines).join(' ') || null;
|
||||
function extractElevation(section) {
|
||||
const guidance = extractGuidance(section);
|
||||
if (!guidance) return null;
|
||||
|
||||
const shadows = [];
|
||||
const seen = new Set();
|
||||
@@ -691,12 +704,7 @@ function extractElevation(section) {
|
||||
for (const inline of extractInlineShadows(b)) dedupe(inline);
|
||||
}
|
||||
|
||||
return {
|
||||
subtitle: section.subtitle,
|
||||
description,
|
||||
shadows,
|
||||
rules: extractNamedRules(section.lines),
|
||||
};
|
||||
return { ...guidance, shadows };
|
||||
}
|
||||
|
||||
function extractInlineShadows(text) {
|
||||
@@ -828,6 +836,15 @@ function extractDosDonts(section) {
|
||||
|
||||
// ---------- Coverage assessment ----------
|
||||
|
||||
// Sections whose model is description-plus-rules only (see extractGuidance).
|
||||
const guidanceCoverage = (guidance) =>
|
||||
guidance
|
||||
? {
|
||||
description: Boolean(guidance.description),
|
||||
rules: guidance.rules.length,
|
||||
}
|
||||
: 'missing';
|
||||
|
||||
function assessCoverage(model) {
|
||||
const report = {};
|
||||
|
||||
@@ -856,6 +873,8 @@ function assessCoverage(model) {
|
||||
}
|
||||
: 'missing';
|
||||
|
||||
report.layout = guidanceCoverage(model.layout);
|
||||
|
||||
report.elevation = model.elevation
|
||||
? {
|
||||
shadows: model.elevation.shadows.length,
|
||||
@@ -864,6 +883,8 @@ function assessCoverage(model) {
|
||||
}
|
||||
: 'missing';
|
||||
|
||||
report.shapes = guidanceCoverage(model.shapes);
|
||||
|
||||
report.components = model.components
|
||||
? {
|
||||
count: model.components.components.length,
|
||||
@@ -893,7 +914,9 @@ export function parseDesignMd(md) {
|
||||
overview: extractOverview(sections['Overview']),
|
||||
colors: extractColors(sections['Colors']),
|
||||
typography: extractTypography(sections['Typography']),
|
||||
layout: extractGuidance(sections['Layout']),
|
||||
elevation: extractElevation(sections['Elevation']),
|
||||
shapes: extractGuidance(sections['Shapes']),
|
||||
components: extractComponents(sections['Components']),
|
||||
dosDonts: extractDosDonts(sections["Do's and Don'ts"]),
|
||||
};
|
||||
|
||||
@@ -11975,7 +11975,9 @@ void main() {
|
||||
rules: [
|
||||
...(md.colors?.rules || []).map((r) => ({ ...r, section: 'colors' })),
|
||||
...(md.typography?.rules || []).map((r) => ({ ...r, section: 'typography' })),
|
||||
...(md.layout?.rules || []).map((r) => ({ ...r, section: 'layout' })),
|
||||
...(md.elevation?.rules || []).map((r) => ({ ...r, section: 'elevation' })),
|
||||
...(md.shapes?.rules || []).map((r) => ({ ...r, section: 'shapes' })),
|
||||
],
|
||||
dos: md.dosDonts?.dos || [],
|
||||
donts: md.dosDonts?.donts || [],
|
||||
|
||||
@@ -873,7 +873,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
||||
// { present, parsed, sidecar, hasMd, hasSidecar,
|
||||
// mdNewerThanJson, parseError?, sidecarError? }
|
||||
// - parsed: output of parseDesignMd (frontmatter
|
||||
// + six canonical sections) when DESIGN.md exists.
|
||||
// + the canonical sections) when DESIGN.md exists.
|
||||
// - sidecar: .impeccable/design.json contents when present.
|
||||
// Expected shape: schemaVersion 2, carrying
|
||||
// extensions + components + narrative.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { parseDesignMd } from '../skill/scripts/lib/design-parser.mjs';
|
||||
import { assessCoverage, parseDesignMd } from '../skill/scripts/lib/design-parser.mjs';
|
||||
|
||||
describe('parseDesignMd frontmatter branch', () => {
|
||||
it('returns null frontmatter when the file has no YAML header', () => {
|
||||
@@ -227,3 +227,89 @@ describe('parseDesignMd overview branch', () => {
|
||||
assert.deepEqual(overview.philosophy, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDesignMd canonical sections', () => {
|
||||
it('preserves content and named rules from all eight canonical sections', () => {
|
||||
const md = `# Design System: Complete
|
||||
|
||||
## Overview
|
||||
|
||||
**Creative North Star: "Structured clarity"**
|
||||
|
||||
## Colors
|
||||
|
||||
### Primary
|
||||
- **Ink** (#111111): Primary text.
|
||||
|
||||
## Typography
|
||||
|
||||
**Body Font:** Inter (with sans-serif)
|
||||
|
||||
## Layout: Responsive rhythm
|
||||
|
||||
Primary regions use a twelve-column grid that collapses to one column on narrow screens.
|
||||
|
||||
### Named Rules
|
||||
**The Spatial Hierarchy Rule.** Primary content must remain visually dominant.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
Surfaces use tonal layering instead of shadows.
|
||||
|
||||
## Shapes
|
||||
|
||||
Selected objects use a double outline and clipped corners.
|
||||
|
||||
### The "Recognizable Silhouette" Rule
|
||||
Repeated geometry must remain recognizable without color.
|
||||
|
||||
## Components
|
||||
|
||||
### Button
|
||||
- **Primary:** Uses the accent color.
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do
|
||||
- Preserve the spatial hierarchy.
|
||||
|
||||
### Don't
|
||||
- Flatten every surface.
|
||||
`;
|
||||
const model = parseDesignMd(md);
|
||||
|
||||
assert.equal(model.layout.subtitle, 'Responsive rhythm');
|
||||
assert.equal(
|
||||
model.layout.description,
|
||||
'Primary regions use a twelve-column grid that collapses to one column on narrow screens.',
|
||||
);
|
||||
assert.deepEqual(model.layout.rules, [{
|
||||
name: 'The Spatial Hierarchy Rule',
|
||||
body: 'Primary content must remain visually dominant.',
|
||||
}]);
|
||||
assert.equal(model.shapes.description, 'Selected objects use a double outline and clipped corners.');
|
||||
assert.deepEqual(model.shapes.rules, [{
|
||||
name: 'The Recognizable Silhouette Rule',
|
||||
body: 'Repeated geometry must remain recognizable without color.',
|
||||
}]);
|
||||
|
||||
const coverage = assessCoverage(model);
|
||||
assert.deepEqual(
|
||||
Object.entries(coverage).filter(([, v]) => v === 'missing').map(([k]) => k),
|
||||
[],
|
||||
'a section present in the markdown must not be reported as missing',
|
||||
);
|
||||
assert.deepEqual(Object.keys(coverage), [
|
||||
'overview',
|
||||
'colors',
|
||||
'typography',
|
||||
'layout',
|
||||
'elevation',
|
||||
'shapes',
|
||||
'components',
|
||||
'dosDonts',
|
||||
]);
|
||||
assert.deepEqual(coverage.layout, { description: true, rules: 1 });
|
||||
assert.deepEqual(coverage.shapes, { description: true, rules: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1333,6 +1333,22 @@ describe('live-browser.js regression guards', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('includes named rules from every canonical narrative section', () => {
|
||||
const narrativeStart = SOURCE.indexOf(' function synthesizeNarrative(parsed) {');
|
||||
const narrativeEnd = SOURCE.indexOf('\n function renderColorTiles', narrativeStart);
|
||||
assert.notEqual(narrativeStart, -1, 'synthesizeNarrative must exist');
|
||||
assert.notEqual(narrativeEnd, -1, 'synthesizeNarrative must end before renderColorTiles');
|
||||
const narrativeSource = SOURCE.slice(narrativeStart, narrativeEnd);
|
||||
|
||||
for (const section of ['colors', 'typography', 'layout', 'elevation', 'shapes']) {
|
||||
assert.match(
|
||||
narrativeSource,
|
||||
new RegExp(`md\\.${section}\\?\\.rules[^\\n]*section: '${section}'`),
|
||||
`the design panel must carry named rules tagged with their ${section} section`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('editing focus timeout does not read a stale inline edit row', () => {
|
||||
assert.doesNotMatch(
|
||||
SOURCE,
|
||||
|
||||
Reference in New Issue
Block a user