-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add design-tree strip functions for ablation experiments #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
f2dec3e
feat: add design-tree strip functions for ablation experiments (#130)
let-sunny ff3179c
fix: address CodeRabbit review on strip functions
let-sunny 43dcb90
chore: re-trigger review
let-sunny adedfd5
fix: protect text content from all strip operations
let-sunny 994bdfe
fix: align-self to layout bucket, trailing component regex, adversari…
let-sunny 023f7d8
fix: escape quotes and backslashes in text content output
let-sunny ccbe21d
fix: narrow overflow strip to only overflow: hidden
let-sunny 21c888e
fix: remove unused variable from overflow strip
let-sunny 5add8db
test: add backslash escaping test for text content
let-sunny d7896c5
fix: escape newlines in text content to prevent parser breakage
let-sunny 07d6f5d
fix: flatten multiline SVG to single line in design-tree output
let-sunny File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,347 @@ | ||
| import { stripDesignTree, DESIGN_TREE_INFO_TYPES } from "./design-tree-strip.js"; | ||
| import type { DesignTreeInfoType } from "./design-tree-strip.js"; | ||
|
|
||
| /** Shared test fixture containing all information types. */ | ||
| const FIXTURE = [ | ||
| "# Design Tree", | ||
| "# Root: 375px x 812px", | ||
| "# Each node shows: name (TYPE, WxH) followed by CSS-like styles", | ||
| "", | ||
| "Page (FRAME, 375x812)", | ||
| ' style: display: flex; flex-direction: column; gap: 16px; padding: 20px 20px 20px 20px; background: #FFFFFF /* var:VariableID:3919:36423 */; overflow: hidden', | ||
| " Header (FRAME, 375x60)", | ||
| ' style: display: flex; flex-direction: row; column-gap: 8px /* var:VariableID:9:11259 */; align-items: center; width: 100%; background: #2C2C2C', | ||
| ' Logo (VECTOR, 40x40)', | ||
| ' style: border: 2px solid #1E1E1E; svg: <svg viewBox="0 0 40 40"><path d="M10 20" fill="#49454F"/></svg>', | ||
| ' Title (TEXT, 200x24)', | ||
| ' style: /* text-style: Heading / Large */; font-family: "Inter"; font-weight: 700; font-size: 24px; line-height: 32px; letter-spacing: -0.5px; text-align: left; color: #FFFFFF /* var:VariableID:3919:36461 */; text: "My Store"', | ||
| " Card (INSTANCE, 335x200) [component: Product Card]", | ||
| " component-properties: Size=Large, HasImage=true", | ||
| ' style: display: flex; flex-direction: column; gap: 12px; padding: 16px 16px 16px 16px; background: #F5F5F5; border-radius: 12px; box-shadow: 0px 4px 8px #000000; min-width: 300px', | ||
| " Image (FRAME, 303x120)", | ||
| " style: width: 100%; height: 100%; background-image: url(images/product@2x.png); background-size: cover; border-radius: 8px; opacity: 0.95", | ||
| " Price (TEXT, 100x20)", | ||
| ' style: font-family: "Inter"; font-weight: 600; font-size: 18px; line-height: 20px; color: rgba(30, 30, 30, 0.8); text: "$29.99"', | ||
| " Button (INSTANCE, 335x48) [component: Button]", | ||
| " component-properties: State=Default, Size=Medium", | ||
| ' style: display: flex; flex-direction: row; justify-content: center; align-items: center; padding: 12px 24px 12px 24px; background: #0066CC; border-radius: 8px; flex-grow: 1; align-self: STRETCH', | ||
| ' [hover]: background: #004499', | ||
| " Label (TEXT, 80x16)", | ||
| ' style: font-family: "Inter"; font-weight: 500; font-size: 16px; line-height: 16px; text-align: center; color: #FFFFFF; text: "Buy Now"', | ||
| ].join("\n"); | ||
|
|
||
| describe("stripDesignTree", () => { | ||
| describe("layout-direction-spacing", () => { | ||
| const result = stripDesignTree(FIXTURE, "layout-direction-spacing"); | ||
|
|
||
| it("removes display, flex-direction, gap, padding, align-items, justify-content", () => { | ||
| expect(result).not.toContain("display: flex"); | ||
| expect(result).not.toContain("flex-direction:"); | ||
| expect(result).not.toContain("gap:"); | ||
| expect(result).not.toContain("column-gap:"); | ||
| expect(result).not.toContain("row-gap:"); | ||
| expect(result).not.toContain("padding:"); | ||
| expect(result).not.toContain("justify-content:"); | ||
| expect(result).not.toContain("align-items:"); | ||
| expect(result).not.toContain("align-self:"); | ||
| }); | ||
|
|
||
| it("preserves colors, typography, and other properties", () => { | ||
| expect(result).toContain("#FFFFFF"); | ||
| expect(result).toContain('font-family: "Inter"'); | ||
| expect(result).toContain("border-radius: 12px"); | ||
| expect(result).toContain("box-shadow:"); | ||
| }); | ||
|
|
||
| it("preserves text content", () => { | ||
| expect(result).toContain('text: "My Store"'); | ||
| expect(result).toContain('text: "$29.99"'); | ||
| }); | ||
|
|
||
| it("preserves node headers", () => { | ||
| expect(result).toContain("Page (FRAME, 375x812)"); | ||
| expect(result).toContain("Card (INSTANCE, 335x200) [component: Product Card]"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("size-constraints", () => { | ||
| const result = stripDesignTree(FIXTURE, "size-constraints"); | ||
|
|
||
| it("removes width: 100%, height: 100%, min-width, flex-grow", () => { | ||
| expect(result).not.toContain("width: 100%"); | ||
| expect(result).not.toContain("height: 100%"); | ||
| expect(result).not.toContain("min-width:"); | ||
| expect(result).not.toContain("flex-grow:"); | ||
| }); | ||
|
|
||
| it("preserves layout and colors", () => { | ||
| expect(result).toContain("display: flex"); | ||
| expect(result).toContain("#FFFFFF"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("color-values", () => { | ||
| const result = stripDesignTree(FIXTURE, "color-values"); | ||
|
|
||
| it("replaces hex colors with [COLOR]", () => { | ||
| expect(result).not.toMatch(/#[0-9A-Fa-f]{6}/); | ||
| expect(result).toContain("background: [COLOR]"); | ||
| expect(result).toContain("color: [COLOR]"); | ||
| }); | ||
|
|
||
| it("replaces rgba colors with [COLOR]", () => { | ||
| expect(result).not.toContain("rgba("); | ||
| expect(result).toContain("color: [COLOR]"); | ||
| }); | ||
|
|
||
| it("replaces colors in border and box-shadow", () => { | ||
| expect(result).toContain("border: 2px solid [COLOR]"); | ||
| expect(result).toContain("box-shadow: 0px 4px 8px [COLOR]"); | ||
| }); | ||
|
|
||
| it("replaces colors in SVG fill/stroke", () => { | ||
| expect(result).toContain('fill="[COLOR]"'); | ||
| }); | ||
|
|
||
| it("replaces colors in [hover] lines", () => { | ||
| expect(result).toContain("[hover]: background: [COLOR]"); | ||
| }); | ||
|
|
||
| it("preserves text content", () => { | ||
| expect(result).toContain('text: "$29.99"'); | ||
| expect(result).toContain('text: "My Store"'); | ||
| }); | ||
|
|
||
| it("preserves background-image", () => { | ||
| expect(result).toContain("background-image: url(images/product@2x.png)"); | ||
| }); | ||
|
|
||
| it("preserves layout and typography", () => { | ||
| expect(result).toContain("display: flex"); | ||
| expect(result).toContain('font-family: "Inter"'); | ||
| }); | ||
| }); | ||
|
|
||
| describe("typography", () => { | ||
| const result = stripDesignTree(FIXTURE, "typography"); | ||
|
|
||
| it("removes font-family, font-weight, font-size, line-height, letter-spacing, text-align, text-decoration", () => { | ||
| expect(result).not.toContain("font-family:"); | ||
| expect(result).not.toContain("font-weight:"); | ||
| expect(result).not.toContain("font-size:"); | ||
| expect(result).not.toContain("line-height:"); | ||
| expect(result).not.toContain("letter-spacing:"); | ||
| expect(result).not.toContain("text-align:"); | ||
| }); | ||
|
|
||
| it("removes text-style comments", () => { | ||
| expect(result).not.toContain("text-style:"); | ||
| }); | ||
|
|
||
| it("preserves text content and colors", () => { | ||
| expect(result).toContain('text: "My Store"'); | ||
| expect(result).toContain("#FFFFFF"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("shadows-effects", () => { | ||
| const result = stripDesignTree(FIXTURE, "shadows-effects"); | ||
|
|
||
| it("removes box-shadow and opacity", () => { | ||
| expect(result).not.toContain("box-shadow:"); | ||
| expect(result).not.toContain("opacity:"); | ||
| }); | ||
|
|
||
| it("preserves everything else", () => { | ||
| expect(result).toContain("display: flex"); | ||
| expect(result).toContain("border-radius: 12px"); | ||
| expect(result).toContain("#F5F5F5"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("component-references", () => { | ||
| const result = stripDesignTree(FIXTURE, "component-references"); | ||
|
|
||
| it("removes [component: ...] from headers", () => { | ||
| expect(result).not.toContain("[component:"); | ||
| expect(result).toContain("Card (INSTANCE, 335x200)"); | ||
| expect(result).toContain("Button (INSTANCE, 335x48)"); | ||
| }); | ||
|
|
||
| it("removes component-properties lines", () => { | ||
| expect(result).not.toContain("component-properties:"); | ||
| }); | ||
|
|
||
| it("preserves styles and text", () => { | ||
| expect(result).toContain("display: flex"); | ||
| expect(result).toContain('text: "Buy Now"'); | ||
| }); | ||
| }); | ||
|
|
||
| describe("component-descriptions", () => { | ||
| it("returns tree unchanged (no-op)", () => { | ||
| expect(stripDesignTree(FIXTURE, "component-descriptions")).toBe(FIXTURE); | ||
| }); | ||
| }); | ||
|
|
||
| describe("node-names-hierarchy", () => { | ||
| const result = stripDesignTree(FIXTURE, "node-names-hierarchy"); | ||
|
|
||
| it("replaces node names with Node1, Node2, etc", () => { | ||
| expect(result).toContain("Node1 (FRAME, 375x812)"); | ||
| expect(result).toContain("Node2 (FRAME, 375x60)"); | ||
| expect(result).not.toContain("Page (FRAME"); | ||
| expect(result).not.toContain("Header (FRAME"); | ||
| expect(result).not.toContain("Card (INSTANCE"); | ||
| }); | ||
|
|
||
| it("preserves comment headers", () => { | ||
| expect(result).toContain("# Design Tree"); | ||
| expect(result).toContain("# Root: 375px x 812px"); | ||
| }); | ||
|
|
||
| it("preserves [component: ...] annotations", () => { | ||
| expect(result).toContain("[component: Product Card]"); | ||
| }); | ||
|
|
||
| it("preserves styles and text content", () => { | ||
| expect(result).toContain("display: flex"); | ||
| expect(result).toContain('text: "My Store"'); | ||
| }); | ||
| }); | ||
|
|
||
| describe("overflow-text-behavior", () => { | ||
| const result = stripDesignTree(FIXTURE, "overflow-text-behavior"); | ||
|
|
||
| it("removes overflow: hidden", () => { | ||
| expect(result).not.toContain("overflow:"); | ||
| }); | ||
|
|
||
| it("preserves everything else", () => { | ||
| expect(result).toContain("display: flex"); | ||
| expect(result).toContain("#FFFFFF"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("hover-interaction-states", () => { | ||
| const result = stripDesignTree(FIXTURE, "hover-interaction-states"); | ||
|
|
||
| it("removes [hover] lines", () => { | ||
| expect(result).not.toContain("[hover]:"); | ||
| }); | ||
|
|
||
| it("preserves all other lines", () => { | ||
| expect(result).toContain("Button (INSTANCE, 335x48) [component: Button]"); | ||
| expect(result).toContain("background: #0066CC"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("design-token-references", () => { | ||
| const result = stripDesignTree(FIXTURE, "design-token-references"); | ||
|
|
||
| it("removes /* var:... */ comments", () => { | ||
| expect(result).not.toContain("/* var:"); | ||
| }); | ||
|
|
||
| it("removes /* text-style: ... */ comments", () => { | ||
| expect(result).not.toContain("/* text-style:"); | ||
| }); | ||
|
|
||
| it("preserves the actual values", () => { | ||
| expect(result).toContain("background: #FFFFFF"); | ||
| expect(result).toContain("color: #FFFFFF"); | ||
| expect(result).toContain('font-family: "Inter"'); | ||
| }); | ||
| }); | ||
|
|
||
| describe("position-stacking", () => { | ||
| it("returns tree unchanged (no-op — design-tree doesn't emit position/z-index)", () => { | ||
| expect(stripDesignTree(FIXTURE, "position-stacking")).toBe(FIXTURE); | ||
| }); | ||
| }); | ||
|
|
||
| describe("general properties", () => { | ||
| it("exports the exact 12 info types in order", () => { | ||
| expect(DESIGN_TREE_INFO_TYPES).toEqual([ | ||
| "layout-direction-spacing", | ||
| "size-constraints", | ||
| "position-stacking", | ||
| "color-values", | ||
| "typography", | ||
| "shadows-effects", | ||
| "component-references", | ||
| "component-descriptions", | ||
| "node-names-hierarchy", | ||
| "overflow-text-behavior", | ||
| "hover-interaction-states", | ||
| "design-token-references", | ||
| ]); | ||
| }); | ||
|
|
||
| it("is idempotent — applying same strip twice gives same result", () => { | ||
| for (const type of DESIGN_TREE_INFO_TYPES) { | ||
| const once = stripDesignTree(FIXTURE, type); | ||
| const twice = stripDesignTree(once, type); | ||
| expect(twice).toBe(once); | ||
| } | ||
| }); | ||
|
|
||
| it("never removes comment headers", () => { | ||
| for (const type of DESIGN_TREE_INFO_TYPES) { | ||
| const result = stripDesignTree(FIXTURE, type); | ||
| expect(result).toContain("# Design Tree"); | ||
| expect(result).toContain("# Root: 375px x 812px"); | ||
| } | ||
| }); | ||
|
|
||
| it("never removes text content", () => { | ||
| for (const type of DESIGN_TREE_INFO_TYPES) { | ||
| const result = stripDesignTree(FIXTURE, type); | ||
| expect(result).toContain('text: "My Store"'); | ||
| expect(result).toContain('text: "$29.99"'); | ||
| expect(result).toContain('text: "Buy Now"'); | ||
| } | ||
| }); | ||
|
|
||
| it("never produces empty output", () => { | ||
| for (const type of DESIGN_TREE_INFO_TYPES) { | ||
| const result = stripDesignTree(FIXTURE, type); | ||
| expect(result.trim().length).toBeGreaterThan(0); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe("adversarial text content", () => { | ||
| const ADVERSARIAL = [ | ||
| "# Design Tree", | ||
| "", | ||
| "Page (FRAME, 375x100)", | ||
| ' style: display: flex; color: #1E1E1E; text: "A; background: #FFFFFF; display: flex"', | ||
| " Info (TEXT, 200x20)", | ||
| ' style: font-size: 16px; text: "svg: <path d=\\"M0 0\\"/>"', | ||
| " Note (TEXT, 200x20)", | ||
| ' style: font-size: 14px; text: "[component: Button] (FRAME, 1x1) /* var:demo */"', | ||
| ].join("\n"); | ||
|
|
||
| it("preserves text with semicolons and CSS-like content", () => { | ||
| for (const type of DESIGN_TREE_INFO_TYPES) { | ||
| const result = stripDesignTree(ADVERSARIAL, type); | ||
| expect(result).toContain('text: "A; background: #FFFFFF; display: flex"'); | ||
| } | ||
| }); | ||
|
|
||
| it("preserves text with SVG-like content", () => { | ||
| for (const type of DESIGN_TREE_INFO_TYPES) { | ||
| const result = stripDesignTree(ADVERSARIAL, type); | ||
| expect(result).toContain('text: "svg: <path d=\\"M0 0\\"/>"'); | ||
| } | ||
| }); | ||
|
|
||
| it("preserves text with component/frame/var-like content", () => { | ||
| for (const type of DESIGN_TREE_INFO_TYPES) { | ||
| const result = stripDesignTree(ADVERSARIAL, type); | ||
| expect(result).toContain('text: "[component: Button] (FRAME, 1x1) /* var:demo */"'); | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.