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]');