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