diff --git a/examples/sidebar-layout.md b/examples/sidebar-layout.md new file mode 100644 index 00000000..05548ede --- /dev/null +++ b/examples/sidebar-layout.md @@ -0,0 +1,55 @@ +::: layout {.sidebar-main} + +## Sidebar {.sidebar} + +:folder: **Projects** + +#### Workspace +[[Overview](./overview.html)]* +[[Projects](./projects.html)] +[[Tasks](./tasks.html)] +[[Calendar](./calendar.html)] + +#### Team +[[Members](./members.html)] +[[Roles](./roles.html)] +[[Activity](./activity.html)] + +#### Settings +[[General](./settings.html)] +[[Billing](./billing.html)] +[[Integrations](./integrations.html)] + +--- + +[[+ New Project](./new-project.html)]* + +## Main {.main} + +## Overview + +A summary of your workspace activity and open tasks. + +## Stats {.grid-3 card} + +### :check: Tasks Done +**48** this week + +### :clock: In Progress +**12** open + +### :bell: Upcoming +**5** due today + +## Recent Projects + +| Project | Owner | Status | Due Date | Progress | +|---------|-------|--------|----------|----------| +| Website Redesign | Alice M. | In Progress | Dec 15 | 64% | +| Mobile App v2 | Bob K. | In Progress | Jan 10 | 31% | +| API Migration | Clara T. | Review | Dec 20 | 88% | +| Design System | Dan W. | Planning | Feb 1 | 12% | + +[[View All Projects →](./projects.html)] + +::: diff --git a/src/parser/transformer.ts b/src/parser/transformer.ts index 6e242d24..51859580 100644 --- a/src/parser/transformer.ts +++ b/src/parser/transformer.ts @@ -29,143 +29,11 @@ export function transformToWiremdAST( theme: 'sketch', }; - const children: WiremdNode[] = []; - - // Visit all nodes in the MDAST with context for dropdown options and grid layouts - let i = 0; - while (i < mdast.children.length) { - const node = mdast.children[i]; - const nextNode = mdast.children[i + 1]; - - // Check if this is a heading with grid class - if (node.type === 'heading') { - const content = extractTextContent(node); - const gridMatch = content.match(/\{[^}]*\.grid-(\d+)[^}]*\}/); - - if (gridMatch) { - const columns = parseInt(gridMatch[1], 10); - const gridHeadingLevel = node.depth; - - // This is a grid container - collect grid items - const gridItems: WiremdNode[] = []; - const headingTransformed = transformHeading(node, options); - - i++; // Move to next node - - // Collect child headings as grid items - while (i < mdast.children.length) { - const childNode = mdast.children[i]; - - // Grid items are headings one level deeper - if ( - childNode.type === 'heading' && - childNode.depth === gridHeadingLevel + 1 - ) { - const gridItem: WiremdNode[] = []; - - // Add the heading - const childNextNode = mdast.children[i + 1]; - const headingNode = transformNode(childNode, options, childNextNode); - if (headingNode) { - gridItem.push(headingNode); - } - - i++; - - // Collect content until next heading at same or higher level - while (i < mdast.children.length) { - const contentNode = mdast.children[i]; - - if ( - contentNode.type === 'heading' && - contentNode.depth <= gridHeadingLevel + 1 - ) { - break; // Stop at next grid item or parent level - } - - const contentNextNode = mdast.children[i + 1]; - const contentTransformed = transformNode(contentNode, options, contentNextNode); - if (contentTransformed) { - gridItem.push(contentTransformed); - - // Skip consumed nodes - if (contentTransformed.type === 'select' && contentNextNode?.type === 'list') { - i++; - } - } - - i++; - } - - // Hoist col-span from heading text to grid-item wrapper - const headingContent = extractTextContent(childNode); - const colSpanMatch = headingContent.match(/\{[^}]*\.col-span-(\d+)[^}]*\}/); - const gridItemProps: any = { classes: [] }; - if (colSpanMatch) { - gridItemProps.classes.push(`col-span-${colSpanMatch[1]}`); - } - - // Add as grid item - gridItems.push({ - type: 'grid-item', - props: gridItemProps, - children: gridItem, - }); - } else if ( - childNode.type === 'heading' && - childNode.depth <= gridHeadingLevel - ) { - // Same or higher level heading — end of grid section - break; - } else if (gridItems.length === 0) { - // Non-heading content before any items — silently skip - i++; - continue; - } else { - // Non-heading content after items — end of grid section - break; - } - } - - // Create grid node - children.push({ - type: 'grid', - columns, - props: (headingTransformed as any).props || {}, - children: gridItems, - }); - - continue; - } - } - - const transformed = transformNode(node, options, nextNode); - if (transformed) { - children.push(transformed); - - // If this was a select node and we consumed the next list, skip it - if (transformed.type === 'select' && nextNode && nextNode.type === 'list') { - i++; // Skip the next node (list) as it was consumed - } - // Also check if it's a container with a select child that has consumed the list - if (transformed.type === 'container' && nextNode && nextNode.type === 'list') { - const hasSelectWithOptions = (transformed.children || []).some((child: any) => - child.type === 'select' && child.options && child.options.length > 0 - ); - if (hasSelectWithOptions) { - i++; // Skip the next node (list) as it was consumed by the select - } - } - } - - i++; - } - return { type: 'document', version: SYNTAX_VERSION, meta, - children, + children: processNodeList(mdast.children as any[], options), }; } @@ -256,27 +124,98 @@ function transformNode( } /** - * Transform container node (:::) + * Process a list of MDAST nodes into wiremd nodes, detecting grid layouts. + * Shared by both the top-level document pass and container children. */ -function transformContainer(node: any, options: ParseOptions): WiremdNode { - const children: WiremdNode[] = []; - const nodeChildren = node.children || []; +function processNodeList(nodeChildren: any[], options: ParseOptions): WiremdNode[] { + const result: WiremdNode[] = []; + let i = 0; - for (let i = 0; i < nodeChildren.length; i++) { - const child = nodeChildren[i]; - const nextChild = nodeChildren[i + 1]; - const transformed = transformNode(child, options, nextChild); + while (i < nodeChildren.length) { + const node = nodeChildren[i]; + const nextNode = nodeChildren[i + 1]; - if (transformed) { - children.push(transformed); + if (node.type === 'heading') { + const content = extractTextContent(node); + const gridMatch = content.match(/\{[^}]*\.grid-(\d+)[^}]*\}/); + + if (gridMatch) { + const columns = parseInt(gridMatch[1], 10); + const gridHeadingLevel = node.depth; + const gridItems: WiremdNode[] = []; + const headingTransformed = transformHeading(node, options); - // Skip next node if it was consumed (dropdown options) - if (transformed.type === 'select' && nextChild && nextChild.type === 'list') { i++; + + while (i < nodeChildren.length) { + const childNode = nodeChildren[i]; + + if (childNode.type === 'heading' && childNode.depth === gridHeadingLevel + 1) { + const gridItem: WiremdNode[] = []; + const childNextNode = nodeChildren[i + 1]; + const headingNode = transformNode(childNode, options, childNextNode); + if (headingNode) gridItem.push(headingNode); + i++; + + while (i < nodeChildren.length) { + const contentNode = nodeChildren[i]; + if (contentNode.type === 'heading' && contentNode.depth <= gridHeadingLevel + 1) break; + const contentNextNode = nodeChildren[i + 1]; + const contentTransformed = transformNode(contentNode, options, contentNextNode); + if (contentTransformed) { + gridItem.push(contentTransformed); + if (contentTransformed.type === 'select' && contentNextNode?.type === 'list') i++; + } + i++; + } + + const headingContent = extractTextContent(childNode); + const colSpanMatch = headingContent.match(/\{[^}]*\.col-span-(\d+)[^}]*\}/); + const gridItemProps: any = { classes: [] }; + if (colSpanMatch) gridItemProps.classes.push(`col-span-${colSpanMatch[1]}`); + + gridItems.push({ type: 'grid-item', props: gridItemProps, children: gridItem }); + } else if (childNode.type === 'heading' && childNode.depth <= gridHeadingLevel) { + break; + } else if (gridItems.length === 0) { + i++; + continue; + } else { + break; + } + } + + result.push({ + type: 'grid', + columns, + props: (headingTransformed as any).props || {}, + children: gridItems, + }); + continue; } } + + const transformed = transformNode(node, options, nextNode); + if (transformed) { + result.push(transformed); + if (transformed.type === 'select' && nextNode && nextNode.type === 'list') i++; + if (transformed.type === 'container' && nextNode && nextNode.type === 'list') { + const hasSelectWithOptions = (transformed.children || []).some((child: any) => + child.type === 'select' && child.options && child.options.length > 0 + ); + if (hasSelectWithOptions) i++; + } + } + i++; } + return result; +} + +/** + * Transform container node (:::) + */ +function transformContainer(node: any, options: ParseOptions): WiremdNode { const props = parseAttributes(node.attributes || ''); const containerType: string = (node.containerType || '').trim(); @@ -284,7 +223,7 @@ function transformContainer(node: any, options: ParseOptions): WiremdNode { type: 'container', containerType: containerType as any, props, - children, + children: processNodeList(node.children || [], options) as any, }; } diff --git a/src/renderer/html-renderer.ts b/src/renderer/html-renderer.ts index d7ce3cba..eb64b90e 100644 --- a/src/renderer/html-renderer.ts +++ b/src/renderer/html-renderer.ts @@ -371,6 +371,12 @@ function renderIcon(node: any, context: RenderContext): string { function renderContainer(node: any, context: RenderContext): string { const { classPrefix: prefix } = context; const classes = buildClasses(prefix, `container-${node.containerType}`, node.props); + + const nodeClasses: string[] = node.props?.classes || []; + if (node.containerType === 'layout' && nodeClasses.includes('sidebar-main')) { + return renderSidebarMainLayout(node, context, classes); + } + const childrenHTML = (node.children || []).map((child: any) => renderNode(child, context)).join('\n '); return `
@@ -378,6 +384,36 @@ function renderContainer(node: any, context: RenderContext): string {
`; } +function renderSidebarMainLayout(node: any, context: RenderContext, classes: string): string { + const { classPrefix: prefix } = context; + const children: any[] = node.children || []; + + const sections: { name: string; nodes: any[] }[] = []; + let current: { name: string; nodes: any[] } | null = null; + + for (const child of children) { + const childClasses: string[] = child.props?.classes || []; + if (child.type === 'heading' && (childClasses.includes('sidebar') || childClasses.includes('main'))) { + if (current) sections.push(current); + current = { name: childClasses.includes('sidebar') ? 'sidebar' : 'main', nodes: [] }; + } else if (current) { + current.nodes.push(child); + } + } + if (current) sections.push(current); + + const sectionsHTML = sections.map((s) => { + const contentHTML = s.nodes.map((child: any) => renderNode(child, context)).join('\n '); + return `
+ ${contentHTML} +
`; + }).join('\n'); + + return `
+${sectionsHTML} +
`; +} + function renderNav(node: any, context: RenderContext): string { const { classPrefix: prefix } = context; const classes = buildClasses(prefix, 'nav', node.props); diff --git a/src/renderer/styles.ts b/src/renderer/styles.ts index 41c570f4..919c8806 100644 --- a/src/renderer/styles.ts +++ b/src/renderer/styles.ts @@ -283,6 +283,68 @@ body.${prefix}root { margin: 0; } +/* Sidebar */ +.${prefix}container-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + width: 180px; + padding: 12px; + background: #f0f8ff; + border: 2px solid #4682B4; + border-radius: 8px; + box-shadow: 3px 3px 0 rgba(0,0,0,0.15); +} +.${prefix}container-sidebar .${prefix}button { + display: block; + width: 100%; + text-align: left; + margin: 0; +} +.${prefix}container-sidebar .${prefix}h4 { + margin: 12px 0 4px; + font-size: 0.85em; + opacity: 0.6; + text-transform: uppercase; +} +.${prefix}container-sidebar .${prefix}separator { + margin: 8px 0; +} + +/* Layout: sidebar-main */ +.${prefix}container-layout.${prefix}sidebar-main { + display: grid; + grid-template-columns: 220px 1fr; + gap: 16px; + align-items: start; +} +.${prefix}layout-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px; + background: #87CEEB; + border: 3px solid #4682B4; + border-radius: 8px; + box-shadow: 3px 3px 0 rgba(0,0,0,0.15); +} +.${prefix}layout-sidebar .${prefix}button { + display: block; + width: 100%; + text-align: left; + margin: 2px 0; +} +.${prefix}layout-sidebar .${prefix}h4 { + margin: 12px 0 4px; + font-size: 0.85em; + opacity: 0.6; + text-transform: uppercase; +} +.${prefix}layout-sidebar .${prefix}separator { margin: 8px 0; } +.${prefix}layout-main { + min-width: 0; +} + /* Grid */ .${prefix}grid { display: grid; @@ -737,6 +799,42 @@ body.${prefix}root { border-color: #343a40; } +/* Sidebar */ +.${prefix}container-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + width: 180px; + padding: 12px; + background: #f0f8ff; + border: 2px solid #4682B4; + border-radius: 8px; +} +.${prefix}container-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 0; } +.${prefix}container-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.85em; opacity: 0.6; text-transform: uppercase; } +.${prefix}container-sidebar .${prefix}separator { margin: 8px 0; } + +/* Layout: sidebar-main */ +.${prefix}container-layout.${prefix}sidebar-main { + display: grid; + grid-template-columns: 220px 1fr; + gap: 16px; + align-items: start; +} +.${prefix}layout-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + padding: 16px 24px; + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 8px; +} +.${prefix}layout-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 2px 0; } +.${prefix}layout-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.85em; opacity: 0.6; text-transform: uppercase; } +.${prefix}layout-sidebar .${prefix}separator { margin: 8px 0; } +.${prefix}layout-main { min-width: 0; } + /* Grid */ .${prefix}grid { display: grid; @@ -1132,6 +1230,40 @@ body.${prefix}root { border-color: #000; } +/* Sidebar */ +.${prefix}container-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + width: 180px; + padding: 12px; + background: #f5f5f5; + border: 1px solid #aaa; +} +.${prefix}container-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 0; } +.${prefix}container-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.85em; opacity: 0.6; text-transform: uppercase; } +.${prefix}container-sidebar .${prefix}separator { margin: 8px 0; } + +/* Layout: sidebar-main */ +.${prefix}container-layout.${prefix}sidebar-main { + display: grid; + grid-template-columns: 220px 1fr; + gap: 16px; + align-items: start; +} +.${prefix}layout-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px 16px; + background: repeating-linear-gradient(90deg, #e0e0e0, #e0e0e0 2px, #d0d0d0 2px, #d0d0d0 4px); + border: 2px solid #000; +} +.${prefix}layout-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 2px 0; } +.${prefix}layout-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.85em; opacity: 0.6; text-transform: uppercase; } +.${prefix}layout-sidebar .${prefix}separator { margin: 8px 0; } +.${prefix}layout-main { min-width: 0; } + /* Grid */ .${prefix}grid { display: grid; @@ -1320,6 +1452,36 @@ body.${prefix}root { margin: 4px 0; } +/* Sidebar */ +.${prefix}container-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + width: 180px; + padding: 8px; +} +.${prefix}container-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 0; } +.${prefix}container-sidebar .${prefix}h4 { margin: 8px 0 4px; font-size: 0.85em; opacity: 0.6; text-transform: uppercase; } +.${prefix}container-sidebar .${prefix}separator { margin: 4px 0; } + +/* Layout: sidebar-main */ +.${prefix}container-layout.${prefix}sidebar-main { + display: grid; + grid-template-columns: 220px 1fr; + gap: 16px; + align-items: start; +} +.${prefix}layout-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + padding: 8px; +} +.${prefix}layout-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 2px 0; } +.${prefix}layout-sidebar .${prefix}h4 { margin: 8px 0 4px; font-size: 0.85em; opacity: 0.6; text-transform: uppercase; } +.${prefix}layout-sidebar .${prefix}separator { margin: 4px 0; } +.${prefix}layout-main { min-width: 0; } + .${prefix}grid { display: grid; grid-template-columns: repeat(var(--grid-columns, 3), 1fr); @@ -1706,6 +1868,41 @@ body { border-bottom: none; } +/* Sidebar */ +.${prefix}container-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + width: 180px; + padding: 12px; + background: #f9fafb; + border-right: 1px solid #e5e7eb; +} +.${prefix}container-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 0; } +.${prefix}container-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.75em; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #6b7280; } +.${prefix}container-sidebar .${prefix}separator { margin: 8px 0; } + +/* Layout: sidebar-main */ +.${prefix}container-layout.${prefix}sidebar-main { + display: grid; + grid-template-columns: 220px 1fr; + gap: 16px; + align-items: start; +} +.${prefix}layout-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + padding: 1rem 1.5rem; + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; +} +.${prefix}layout-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 2px 0; } +.${prefix}layout-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.75em; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #6b7280; } +.${prefix}layout-sidebar .${prefix}separator { margin: 8px 0; } +.${prefix}layout-main { min-width: 0; } + /* Grid */ .${prefix}grid { display: grid; @@ -2322,6 +2519,40 @@ body { border-bottom: none; } +/* Sidebar */ +.${prefix}container-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + width: 180px; + padding: 12px; + background: #fafafa; + border-right: 1px solid #e0e0e0; +} +.${prefix}container-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 0; } +.${prefix}container-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.75em; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: rgba(0,0,0,0.54); } +.${prefix}container-sidebar .${prefix}separator { margin: 8px 0; } + +/* Layout: sidebar-main */ +.${prefix}container-layout.${prefix}sidebar-main { + display: grid; + grid-template-columns: 220px 1fr; + gap: 16px; + align-items: start; +} +.${prefix}layout-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + padding: 0 16px 16px; + background: #ede7f6; + border-right: 4px solid #6200ee; +} +.${prefix}layout-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 2px 0; } +.${prefix}layout-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.75em; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: rgba(0,0,0,0.54); } +.${prefix}layout-sidebar .${prefix}separator { margin: 8px 0; } +.${prefix}layout-main { min-width: 0; } + /* Material Grid */ .${prefix}grid { display: grid; @@ -2939,6 +3170,42 @@ body { border-bottom: none; } +/* Sidebar */ +.${prefix}container-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + width: 180px; + padding: 12px; + background: #fff; + border: 3px solid #000; + box-shadow: 4px 4px 0 #000; +} +.${prefix}container-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 0; } +.${prefix}container-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.85em; font-weight: 900; text-transform: uppercase; } +.${prefix}container-sidebar .${prefix}separator { margin: 8px 0; } + +/* Layout: sidebar-main */ +.${prefix}container-layout.${prefix}sidebar-main { + display: grid; + grid-template-columns: 220px 1fr; + gap: 16px; + align-items: start; +} +.${prefix}layout-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + padding: 16px 20px; + background: #ffd93d; + border: 3px solid #000; + box-shadow: 4px 4px 0 #000; +} +.${prefix}layout-sidebar .${prefix}button { display: block; width: 100%; text-align: left; margin: 2px 0; } +.${prefix}layout-sidebar .${prefix}h4 { margin: 12px 0 4px; font-size: 0.85em; font-weight: 900; text-transform: uppercase; } +.${prefix}layout-sidebar .${prefix}separator { margin: 8px 0; } +.${prefix}layout-main { min-width: 0; } + /* Brutal Grid */ .${prefix}grid { display: grid; diff --git a/tests/parser.test.ts b/tests/parser.test.ts index ff921f28..94efa9ad 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -516,6 +516,90 @@ Quick const result = parse(input); expect((result.children[0] as any).props.card).toBeFalsy(); }); + + it('should parse a grid nested inside a container as a grid node', () => { + const input = ` +::: card + +## Features {.grid-3} + +### Fast +Quick + +### Secure +Safe + +### Powerful +Strong + +::: + `.trim(); + + const result = parse(input); + const card = result.children[0] as any; + expect(card.type).toBe('container'); + expect(card.containerType).toBe('card'); + // The grid must be a grid node, not a plain heading + const grid = card.children[0]; + expect(grid.type).toBe('grid'); + expect(grid.columns).toBe(3); + expect(grid.children).toHaveLength(3); + expect(grid.children[0].type).toBe('grid-item'); + }); + }); + + describe('Sidebar layout', () => { + it('should parse :::layout {.sidebar-main} as a layout container', () => { + const input = ` +::: layout {.sidebar-main} + +## Sidebar {.sidebar} +Nav here + +## Main {.main} +Content here + +::: + `.trim(); + + const result = parse(input); + const layout = result.children[0] as any; + expect(layout.type).toBe('container'); + expect(layout.containerType).toBe('layout'); + expect(layout.props.classes).toContain('sidebar-main'); + }); + + it('should parse grid inside sidebar-main layout as a grid node', () => { + const input = ` +::: layout {.sidebar-main} + +## Sidebar {.sidebar} +Nav + +## Main {.main} + +## Stats {.grid-3} + +### Done +48 + +### Active +12 + +### Pending +5 + +::: + `.trim(); + + const result = parse(input); + const layout = result.children[0] as any; + // Find the grid in the layout's children + const grid = layout.children.find((c: any) => c.type === 'grid'); + expect(grid).toBeDefined(); + expect(grid.columns).toBe(3); + expect(grid.children).toHaveLength(3); + }); }); describe('Button link syntax [[text](url)]', () => { diff --git a/tests/renderer.test.ts b/tests/renderer.test.ts index d24a39c4..fce5bd32 100644 --- a/tests/renderer.test.ts +++ b/tests/renderer.test.ts @@ -416,6 +416,141 @@ Email }); }); + describe('Grid inside container', () => { + it('should render a grid nested inside a card container', () => { + const input = ` +::: card + +## Features {.grid-3} + +### Fast +Quick + +### Secure +Safe + +### Powerful +Strong + +::: + `.trim(); + const ast = parse(input); + const html = renderToHTML(ast, { style: 'sketch' }); + // Must produce an actual grid div, not just CSS class strings + expect(html).toMatch(/
/); + expect(html).toContain('Fast'); + expect(html).toContain('Secure'); + expect(html).toContain('Powerful'); + }); + + it('should render a grid nested inside sidebar-main layout main section', () => { + const input = ` +::: layout {.sidebar-main} + +## Sidebar {.sidebar} +Nav + +## Main {.main} + +## Stats {.grid-3} + +### Done +48 + +### Active +12 + +### Pending +5 + +::: + `.trim(); + const ast = parse(input); + const html = renderToHTML(ast, { style: 'sketch' }); + expect(html).toMatch(/
/); + expect(html).toContain('Done'); + expect(html).toContain('Active'); + expect(html).toContain('Pending'); + }); + }); + + describe('Sidebar Layout', () => { + const sidebarInput = ` +::: layout {.sidebar-main} + +## Sidebar {.sidebar} +- [Home](#) +- [Settings](#) + +## Main {.main} +### Dashboard +Content here + +::: + `.trim(); + + it('should render sidebar-main layout with correct wrapper class', () => { + const ast = parse(sidebarInput); + const html = renderToHTML(ast, { style: 'sketch' }); + expect(html).toContain('wmd-container-layout'); + expect(html).toContain('wmd-sidebar-main'); + }); + + it('should render sidebar and main sections as separate divs', () => { + const ast = parse(sidebarInput); + const html = renderToHTML(ast, { style: 'sketch' }); + expect(html).toContain('wmd-layout-sidebar'); + expect(html).toContain('wmd-layout-main'); + }); + + it('should NOT render the section heading labels as visible text', () => { + const ast = parse(sidebarInput); + const html = renderToHTML(ast, { style: 'sketch' }); + // Heading text "Sidebar" and "Main" used as structural markers should not appear in output + expect(html).not.toMatch(/]*>[^<]*\bSidebar\b/); + expect(html).not.toMatch(/]*>[^<]*\bMain\b/); + }); + + it('should place sidebar content inside layout-sidebar div', () => { + const ast = parse(sidebarInput); + const html = renderToHTML(ast, { style: 'sketch' }); + // Extract just the body content (after ) to avoid CSS false positives + const bodyStart = html.indexOf(''); + const body = html.slice(bodyStart); + const sidebarDivPos = body.indexOf('class="wmd-layout-sidebar"'); + const mainDivPos = body.indexOf('class="wmd-layout-main"'); + const homePos = body.indexOf('>Home<'); + expect(sidebarDivPos).toBeGreaterThan(-1); + expect(homePos).toBeGreaterThan(sidebarDivPos); + expect(homePos).toBeLessThan(mainDivPos); + }); + + it('should place main content inside layout-main div', () => { + const ast = parse(sidebarInput); + const html = renderToHTML(ast, { style: 'sketch' }); + const bodyStart = html.indexOf(''); + const body = html.slice(bodyStart); + const mainDivPos = body.indexOf('class="wmd-layout-main"'); + const dashPos = body.indexOf('Dashboard'); + expect(dashPos).toBeGreaterThan(mainDivPos); + }); + + it('should include sidebar-main grid CSS', () => { + const ast = parse(sidebarInput); + const html = renderToHTML(ast, { style: 'sketch' }); + expect(html).toMatch(/grid-template-columns:\s*\d+px\s+1fr/); + }); + + it('should include container-sidebar CSS', () => { + const ast = parse('::: sidebar\n- [Home](#)\n:::\n'); + const html = renderToHTML(ast, { style: 'sketch' }); + expect(html).toContain('wmd-container-sidebar'); + expect(html).toMatch(/\.wmd-container-sidebar\s*\{/); + }); + }); + describe('JSON Renderer', () => { it('should render to JSON', () => { const ast = parse('[Button]');