@protomarkdown/parser 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -56,6 +56,13 @@ class MarkdownParser {
56
56
  });
57
57
  continue;
58
58
  }
59
+ // Check for workflow start
60
+ if (line === '[workflow' || line.startsWith('[workflow ')) {
61
+ const result = this.parseWorkflow(lines, i);
62
+ nodes.push(result.node);
63
+ i = result.nextIndex;
64
+ continue;
65
+ }
59
66
  // Check for card start
60
67
  const cardMatch = line.match(/^\[--\s*(.*)$/);
61
68
  if (cardMatch) {
@@ -91,6 +98,100 @@ class MarkdownParser {
91
98
  }
92
99
  return { nodes, errors: errors.length > 0 ? errors : undefined };
93
100
  }
101
+ parseWorkflow(lines, startIndex) {
102
+ const screens = [];
103
+ let i = startIndex + 1;
104
+ let depth = 1;
105
+ let initialScreen;
106
+ // Parse workflow content until we find the matching closing ]
107
+ while (i < lines.length && depth > 0) {
108
+ const workflowLine = this.options.preserveWhitespace ? lines[i] : lines[i].trim();
109
+ // Check for workflow closing
110
+ if (workflowLine === "]") {
111
+ depth--;
112
+ if (depth === 0) {
113
+ break;
114
+ }
115
+ }
116
+ // Check for screen opening ([screen id)
117
+ const screenMatch = workflowLine.match(/^\[screen\s+(.+)$/);
118
+ if (screenMatch) {
119
+ const screenId = screenMatch[1].trim();
120
+ const result = this.parseScreen(lines, i, screenId);
121
+ screens.push(result.node);
122
+ // First screen becomes the initial screen
123
+ if (!initialScreen) {
124
+ initialScreen = screenId;
125
+ }
126
+ i = result.nextIndex;
127
+ continue;
128
+ }
129
+ i++;
130
+ }
131
+ return {
132
+ node: {
133
+ type: "workflow",
134
+ children: screens,
135
+ initialScreen: initialScreen || (screens[0]?.id),
136
+ },
137
+ nextIndex: i + 1,
138
+ };
139
+ }
140
+ parseScreen(lines, startIndex, screenId) {
141
+ const screenChildren = [];
142
+ let i = startIndex + 1;
143
+ let depth = 1;
144
+ // Parse screen content until we find the matching closing ]
145
+ while (i < lines.length && depth > 0) {
146
+ const screenLine = this.options.preserveWhitespace ? lines[i] : lines[i].trim();
147
+ // Check for screen closing
148
+ if (screenLine === "]") {
149
+ depth--;
150
+ if (depth === 0) {
151
+ break;
152
+ }
153
+ }
154
+ // Check for nested card opening
155
+ if (screenLine.match(/^\[--\s*(.*)$/)) {
156
+ const nestedTitle = screenLine.match(/^\[--\s*(.*)$/)?.[1] || undefined;
157
+ const result = this.parseCard(lines, i, nestedTitle);
158
+ screenChildren.push(result.node);
159
+ i = result.nextIndex;
160
+ continue;
161
+ }
162
+ // Check for nested grid opening
163
+ if (screenLine.match(/^\[grid\s+(.*)$/)) {
164
+ const nestedConfig = screenLine.match(/^\[grid\s+(.*)$/)?.[1] || '';
165
+ const result = this.parseContainer(lines, i, 'grid', nestedConfig);
166
+ screenChildren.push(result.node);
167
+ i = result.nextIndex;
168
+ continue;
169
+ }
170
+ // Check for nested div opening
171
+ if (screenLine.match(/^\[\s*(.*)$/) && !screenLine.includes("]")) {
172
+ const nestedConfig = screenLine.match(/^\[\s*(.*)$/)?.[1] || '';
173
+ const result = this.parseContainer(lines, i, 'div', nestedConfig);
174
+ screenChildren.push(result.node);
175
+ i = result.nextIndex;
176
+ continue;
177
+ }
178
+ if (screenLine) {
179
+ const childNode = this.parseLine(screenLine);
180
+ if (childNode) {
181
+ screenChildren.push(childNode);
182
+ }
183
+ }
184
+ i++;
185
+ }
186
+ return {
187
+ node: {
188
+ type: "screen",
189
+ id: screenId,
190
+ children: screenChildren,
191
+ },
192
+ nextIndex: i + 1,
193
+ };
194
+ }
94
195
  parseCard(lines, startIndex, title) {
95
196
  const cardChildren = [];
96
197
  let i = startIndex + 1;
@@ -355,13 +456,26 @@ class MarkdownParser {
355
456
  };
356
457
  }
357
458
  // Parse multiple buttons on one line ([btn1][(btn2)])
358
- const multiButtonMatch = line.match(/^(\[\(?[^\[\]|]+\)?\]\s*)+$/);
459
+ const multiButtonMatch = line.match(/^(\[\(?[^\[\]]+\)?\]\s*)+$/);
359
460
  if (multiButtonMatch) {
360
- const buttons = line.match(/\[(\(?)[^\[\]|]+?(\)?)\]/g);
461
+ const buttons = line.match(/\[(\(?)[^\[\]]+?(\)?)\]/g);
361
462
  if (buttons && buttons.length > 1) {
362
463
  return {
363
464
  type: "container",
364
465
  children: buttons.map((btn) => {
466
+ // Check for navigation syntax: [(text) -> target] or [text -> target]
467
+ const navMatch = btn.match(/\[(\(?)(.+?)(\)?)\s*->\s*([^\]]+)\]/);
468
+ if (navMatch) {
469
+ const isDefault = navMatch[1] === "(" && navMatch[3] === ")";
470
+ const content = navMatch[2].trim();
471
+ const navigateTo = navMatch[4].trim();
472
+ return {
473
+ type: "button",
474
+ content,
475
+ variant: isDefault ? "default" : "outline",
476
+ navigateTo,
477
+ };
478
+ }
365
479
  const innerMatch = btn.match(/\[(\(?)(.+?)(\)?)\]/);
366
480
  if (innerMatch) {
367
481
  const isDefault = innerMatch[1] === "(" && innerMatch[3] === ")";
@@ -381,6 +495,18 @@ class MarkdownParser {
381
495
  };
382
496
  }
383
497
  }
498
+ // Parse default button with navigation [(button text) -> target]
499
+ const defaultButtonNavMatch = line.match(/^\[\((.+?)\)\s*->\s*([^\]]+)\]$/);
500
+ if (defaultButtonNavMatch) {
501
+ const content = defaultButtonNavMatch[1].trim();
502
+ const navigateTo = defaultButtonNavMatch[2].trim();
503
+ return {
504
+ type: "button",
505
+ content,
506
+ variant: "default",
507
+ navigateTo,
508
+ };
509
+ }
384
510
  // Parse default button [(button text)] or [(button text) | classes]
385
511
  const defaultButtonMatch = line.match(/^\[\((.+?)\)(?:\s*\|\s*(.+))?\]$/);
386
512
  if (defaultButtonMatch) {
@@ -393,6 +519,18 @@ class MarkdownParser {
393
519
  ...(className && { className }),
394
520
  };
395
521
  }
522
+ // Parse outline button with navigation [button text -> target]
523
+ const buttonNavMatch = line.match(/^\[([^|]+?)\s*->\s*([^\]]+)\]$/);
524
+ if (buttonNavMatch) {
525
+ const content = buttonNavMatch[1].trim();
526
+ const navigateTo = buttonNavMatch[2].trim();
527
+ return {
528
+ type: "button",
529
+ content,
530
+ variant: "outline",
531
+ navigateTo,
532
+ };
533
+ }
396
534
  // Parse outline button [button text] or [button text | classes]
397
535
  const buttonMatch = line.match(/^\[([^|]+?)(?:\s*\|\s*(.+))?\]$/);
398
536
  if (buttonMatch) {
@@ -501,6 +639,8 @@ class ShadcnCodeGenerator {
501
639
  generate(nodes) {
502
640
  this.indentLevel = 0;
503
641
  this.requiredImports.clear();
642
+ // Check if the component contains a workflow
643
+ const hasWorkflow = nodes.some(node => node.type === 'workflow');
504
644
  // Generate component body
505
645
  const componentBody = this.generateNodes(nodes);
506
646
  // Add base indentation (6 spaces = 3 levels for proper JSX nesting)
@@ -510,7 +650,8 @@ class ShadcnCodeGenerator {
510
650
  .join('\n');
511
651
  // Collect imports
512
652
  const imports = this.generateImports();
513
- return `${imports}
653
+ const reactImport = hasWorkflow ? "import { useState } from 'react';\n" : "";
654
+ return `${reactImport}${imports}
514
655
 
515
656
  export function GeneratedComponent() {
516
657
  return (
@@ -598,6 +739,10 @@ ${indentedBody}
598
739
  return this.generateItalic(node, index);
599
740
  case "image":
600
741
  return this.generateImage(node, index);
742
+ case "workflow":
743
+ return this.generateWorkflow(node, index);
744
+ case "screen":
745
+ return this.generateScreen(node, index);
601
746
  default:
602
747
  return "";
603
748
  }
@@ -713,7 +858,9 @@ ${this.indent()}</div>`;
713
858
  this.requiredImports.add("Button");
714
859
  const variant = node.variant || "default";
715
860
  const className = node.className ? ` className="${node.className}"` : "";
716
- return `${this.indent()}<Button key={${index}} variant="${variant}"${className}>${this.escapeJSX(node.content || "")}</Button>`;
861
+ // Add onClick handler if button has navigation
862
+ const onClick = node.navigateTo ? ` onClick={() => setCurrentScreen('${node.navigateTo}')}` : "";
863
+ return `${this.indent()}<Button key={${index}} variant="${variant}"${className}${onClick}>${this.escapeJSX(node.content || "")}</Button>`;
717
864
  }
718
865
  generateContainer(node, index) {
719
866
  this.indentLevel++;
@@ -814,7 +961,312 @@ ${this.indent()}</div>`;
814
961
  const alt = node.alt || "";
815
962
  return `${this.indent()}<img key={${index}} src="${src}" alt="${this.escapeJSX(alt)}" className="max-w-full h-auto" />`;
816
963
  }
964
+ generateWorkflow(node, index) {
965
+ const screens = node.children || [];
966
+ const initialScreen = node.initialScreen || screens[0]?.id || "home";
967
+ // Generate state management
968
+ const stateDeclaration = `${this.indent()}const [currentScreen, setCurrentScreen] = useState('${initialScreen}');`;
969
+ // Generate screen rendering
970
+ this.indentLevel++;
971
+ const screenCases = screens.map((screen, i) => {
972
+ const screenId = screen.id || `screen-${i}`;
973
+ const screenContent = screen.children ? this.generateNodes(screen.children) : "";
974
+ return `${this.indent()}${i === 0 ? '' : 'else '}if (currentScreen === '${screenId}') {
975
+ ${this.indent()} return (
976
+ ${this.indent()} <div className="space-y-2">
977
+ ${screenContent}
978
+ ${this.indent()} </div>
979
+ ${this.indent()} );
980
+ ${this.indent()}}`;
981
+ }).join('\n');
982
+ this.indentLevel--;
983
+ // Return fallback if no screen matches
984
+ const fallback = `${this.indent()}return <div>Screen not found</div>;`;
985
+ return `${this.indent()}<div key={${index}}>
986
+ ${this.indent()} {(() => {
987
+ ${stateDeclaration}
988
+
989
+ ${screenCases}
990
+ ${fallback}
991
+ ${this.indent()} })()}
992
+ ${this.indent()}</div>`;
993
+ }
994
+ generateScreen(node, index) {
995
+ // Screens are handled within workflow generation
996
+ // This method is for standalone screen nodes (if used outside workflow)
997
+ this.indentLevel++;
998
+ const children = node.children ? this.generateNodes(node.children) : "";
999
+ this.indentLevel--;
1000
+ return `${this.indent()}<div key={${index}} data-screen-id="${node.id || index}" className="space-y-2">
1001
+ ${children}
1002
+ ${this.indent()}</div>`;
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * Generates HTML from a Proto Markdown AST
1008
+ * Used for VS Code extension preview rendering
1009
+ */
1010
+ class HtmlGenerator {
1011
+ /**
1012
+ * Generate HTML from markdown AST
1013
+ */
1014
+ generate(nodes) {
1015
+ return nodes.map((node) => this.renderNode(node)).join("\n");
1016
+ }
1017
+ renderNode(node) {
1018
+ switch (node.type) {
1019
+ case "header":
1020
+ return this.renderHeader(node);
1021
+ case "text":
1022
+ return this.renderText(node);
1023
+ case "bold":
1024
+ return this.renderBold(node);
1025
+ case "italic":
1026
+ return this.renderItalic(node);
1027
+ case "input":
1028
+ return this.renderInput(node);
1029
+ case "textarea":
1030
+ return this.renderTextarea(node);
1031
+ case "checkbox":
1032
+ return this.renderCheckbox(node);
1033
+ case "radiogroup":
1034
+ return this.renderRadioGroup(node);
1035
+ case "dropdown":
1036
+ return this.renderDropdown(node);
1037
+ case "button":
1038
+ return this.renderButton(node);
1039
+ case "card":
1040
+ return this.renderCard(node);
1041
+ case "container":
1042
+ return this.renderContainer(node);
1043
+ case "grid":
1044
+ return this.renderGrid(node);
1045
+ case "div":
1046
+ return this.renderDiv(node);
1047
+ case "table":
1048
+ return this.renderTable(node);
1049
+ case "image":
1050
+ return this.renderImage(node);
1051
+ case "workflow":
1052
+ return this.renderWorkflow(node);
1053
+ case "screen":
1054
+ return this.renderScreen(node);
1055
+ default:
1056
+ return `<div class="proto-unknown">${JSON.stringify(node)}</div>`;
1057
+ }
1058
+ }
1059
+ renderHeader(node) {
1060
+ const level = node.level || 1;
1061
+ let content;
1062
+ if (node.children && node.children.length > 0) {
1063
+ content = this.renderInlineNodes(node.children);
1064
+ }
1065
+ else {
1066
+ content = this.escapeHtml(node.content || "");
1067
+ }
1068
+ return `<h${level} class="proto-header">${content}</h${level}>`;
1069
+ }
1070
+ renderText(node) {
1071
+ if (node.children && node.children.length > 0) {
1072
+ const content = this.renderInlineNodes(node.children);
1073
+ return `<p class="proto-text">${content}</p>`;
1074
+ }
1075
+ return `<p class="proto-text">${this.escapeHtml(node.content || "")}</p>`;
1076
+ }
1077
+ renderBold(node) {
1078
+ if (node.children && node.children.length > 0) {
1079
+ return `<strong>${this.renderInlineNodes(node.children)}</strong>`;
1080
+ }
1081
+ return `<strong>${this.escapeHtml(node.content || "")}</strong>`;
1082
+ }
1083
+ renderItalic(node) {
1084
+ if (node.children && node.children.length > 0) {
1085
+ return `<em>${this.renderInlineNodes(node.children)}</em>`;
1086
+ }
1087
+ return `<em>${this.escapeHtml(node.content || "")}</em>`;
1088
+ }
1089
+ renderInlineNodes(nodes) {
1090
+ return nodes.map((node) => this.renderInlineNode(node)).join("");
1091
+ }
1092
+ renderInlineNode(node) {
1093
+ switch (node.type) {
1094
+ case "bold":
1095
+ if (node.children && node.children.length > 0) {
1096
+ return `<strong>${this.renderInlineNodes(node.children)}</strong>`;
1097
+ }
1098
+ return `<strong>${this.escapeHtml(node.content || "")}</strong>`;
1099
+ case "italic":
1100
+ if (node.children && node.children.length > 0) {
1101
+ return `<em>${this.renderInlineNodes(node.children)}</em>`;
1102
+ }
1103
+ return `<em>${this.escapeHtml(node.content || "")}</em>`;
1104
+ case "text":
1105
+ if (node.children && node.children.length > 0) {
1106
+ return this.renderInlineNodes(node.children);
1107
+ }
1108
+ return this.escapeHtml(node.content || "");
1109
+ default:
1110
+ return this.escapeHtml(node.content || "");
1111
+ }
1112
+ }
1113
+ renderInput(node) {
1114
+ const placeholder = node.inputType === "password" ? "••••••••" : "";
1115
+ return `
1116
+ <div class="proto-field">
1117
+ <label class="proto-label">${this.escapeHtml(node.label || "")}</label>
1118
+ <input type="${node.inputType || "text"}" class="proto-input" placeholder="${placeholder}" disabled />
1119
+ </div>`;
1120
+ }
1121
+ renderTextarea(node) {
1122
+ return `
1123
+ <div class="proto-field">
1124
+ <label class="proto-label">${this.escapeHtml(node.label || "")}</label>
1125
+ <textarea class="proto-textarea" disabled></textarea>
1126
+ </div>`;
1127
+ }
1128
+ renderCheckbox(node) {
1129
+ return `
1130
+ <div class="proto-checkbox">
1131
+ <input type="checkbox" class="proto-checkbox-input" disabled />
1132
+ <label class="proto-checkbox-label">${this.escapeHtml(node.label || "")}</label>
1133
+ </div>`;
1134
+ }
1135
+ renderRadioGroup(node) {
1136
+ const options = (node.options || [])
1137
+ .map((opt) => `
1138
+ <div class="proto-radio-option">
1139
+ <input type="radio" class="proto-radio-input" name="${this.escapeHtml(node.label || "")}" disabled />
1140
+ <label class="proto-radio-label">${this.escapeHtml(opt)}</label>
1141
+ </div>`)
1142
+ .join("");
1143
+ return `
1144
+ <div class="proto-radiogroup">
1145
+ <label class="proto-label">${this.escapeHtml(node.label || "")}</label>
1146
+ <div class="proto-radio-options">${options}</div>
1147
+ </div>`;
1148
+ }
1149
+ renderDropdown(node) {
1150
+ const options = (node.options || ["Select an option"])
1151
+ .map((opt) => `<option>${this.escapeHtml(opt)}</option>`)
1152
+ .join("");
1153
+ return `
1154
+ <div class="proto-field">
1155
+ <label class="proto-label">${this.escapeHtml(node.label || "")}</label>
1156
+ <select class="proto-select" disabled>${options}</select>
1157
+ </div>`;
1158
+ }
1159
+ renderButton(node) {
1160
+ const btnClass = node.variant === "default"
1161
+ ? "proto-button-default"
1162
+ : "proto-button-outline";
1163
+ const navIndicator = node.navigateTo
1164
+ ? ` <span class="proto-nav-indicator">→ ${this.escapeHtml(node.navigateTo)}</span>`
1165
+ : "";
1166
+ return `<button class="proto-button ${btnClass}" disabled>${this.escapeHtml(node.content || "")}${navIndicator}</button>`;
1167
+ }
1168
+ renderCard(node) {
1169
+ let cardTitle = "";
1170
+ if (node.titleChildren && node.titleChildren.length > 0) {
1171
+ cardTitle = `<div class="proto-card-header">${this.renderInlineNodes(node.titleChildren)}</div>`;
1172
+ }
1173
+ else if (node.title) {
1174
+ cardTitle = `<div class="proto-card-header">${this.escapeHtml(node.title)}</div>`;
1175
+ }
1176
+ const cardChildren = node.children ? this.generate(node.children) : "";
1177
+ return `
1178
+ <div class="proto-card">
1179
+ ${cardTitle}
1180
+ <div class="proto-card-content">${cardChildren}</div>
1181
+ </div>`;
1182
+ }
1183
+ renderContainer(node) {
1184
+ const children = node.children ? this.generate(node.children) : "";
1185
+ return `<div class="proto-container">${children}</div>`;
1186
+ }
1187
+ renderGrid(node) {
1188
+ const children = node.children ? this.generate(node.children) : "";
1189
+ const gridConfig = this.parseGridConfig(node.gridConfig || "");
1190
+ return `<div class="proto-grid" style="${gridConfig}">${children}</div>`;
1191
+ }
1192
+ renderDiv(node) {
1193
+ const children = node.children ? this.generate(node.children) : "";
1194
+ return `<div class="proto-div ${this.escapeHtml(node.className || "")}">${children}</div>`;
1195
+ }
1196
+ renderTable(node) {
1197
+ const headerCells = (node.headers || [])
1198
+ .map((h) => `<th class="proto-table-th">${this.escapeHtml(h)}</th>`)
1199
+ .join("");
1200
+ const bodyRows = (node.rows || [])
1201
+ .map((row) => `<tr>${row
1202
+ .map((cell) => `<td class="proto-table-td">${this.escapeHtml(cell)}</td>`)
1203
+ .join("")}</tr>`)
1204
+ .join("");
1205
+ return `
1206
+ <table class="proto-table">
1207
+ <thead><tr>${headerCells}</tr></thead>
1208
+ <tbody>${bodyRows}</tbody>
1209
+ </table>`;
1210
+ }
1211
+ renderImage(node) {
1212
+ return `<img class="proto-image" src="${this.escapeHtml(node.src || "")}" alt="${this.escapeHtml(node.alt || "")}" />`;
1213
+ }
1214
+ renderWorkflow(node) {
1215
+ const screens = (node.children || [])
1216
+ .map((screen, idx) => {
1217
+ const isInitial = screen.id === node.initialScreen || idx === 0;
1218
+ const screenContent = screen.children
1219
+ ? this.generate(screen.children)
1220
+ : "";
1221
+ const screenId = screen.id || "";
1222
+ return `
1223
+ <div class="proto-screen${isInitial ? " proto-screen-active" : ""}" data-screen-id="${this.escapeHtml(screenId)}">
1224
+ <div class="proto-screen-header">
1225
+ <span class="proto-screen-badge">${this.escapeHtml(screenId)}</span>
1226
+ ${isInitial
1227
+ ? '<span class="proto-screen-initial">Initial</span>'
1228
+ : ""}
1229
+ </div>
1230
+ <div class="proto-screen-content">${screenContent}</div>
1231
+ </div>`;
1232
+ })
1233
+ .join("");
1234
+ return `<div class="proto-workflow">${screens}</div>`;
1235
+ }
1236
+ renderScreen(node) {
1237
+ const screenChildren = node.children ? this.generate(node.children) : "";
1238
+ const screenId = node.id || "";
1239
+ return `
1240
+ <div class="proto-screen" data-screen-id="${this.escapeHtml(screenId)}">
1241
+ <div class="proto-screen-header">
1242
+ <span class="proto-screen-badge">${this.escapeHtml(screenId)}</span>
1243
+ </div>
1244
+ <div class="proto-screen-content">${screenChildren}</div>
1245
+ </div>`;
1246
+ }
1247
+ parseGridConfig(config) {
1248
+ const styles = [];
1249
+ // Parse cols-N
1250
+ const colsMatch = config.match(/cols-(\d+)/);
1251
+ if (colsMatch) {
1252
+ styles.push(`grid-template-columns: repeat(${colsMatch[1]}, 1fr)`);
1253
+ }
1254
+ // Parse gap-N
1255
+ const gapMatch = config.match(/gap-(\d+)/);
1256
+ if (gapMatch) {
1257
+ styles.push(`gap: ${parseInt(gapMatch[1]) * 4}px`);
1258
+ }
1259
+ return styles.join("; ");
1260
+ }
1261
+ escapeHtml(text) {
1262
+ return text
1263
+ .replace(/&/g, "&amp;")
1264
+ .replace(/</g, "&lt;")
1265
+ .replace(/>/g, "&gt;")
1266
+ .replace(/"/g, "&quot;")
1267
+ .replace(/'/g, "&#039;");
1268
+ }
817
1269
  }
818
1270
 
819
- export { MarkdownParser, ShadcnCodeGenerator };
1271
+ export { HtmlGenerator, MarkdownParser, ShadcnCodeGenerator };
820
1272
  //# sourceMappingURL=index.esm.js.map