wyreframe 0.1.0
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/README.md +123 -0
- package/dist/index.d.ts +267 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +195 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/src/parser/Core/Bounds.mjs +61 -0
- package/src/parser/Core/Bounds.res +65 -0
- package/src/parser/Core/Grid.mjs +268 -0
- package/src/parser/Core/Grid.res +265 -0
- package/src/parser/Core/Position.mjs +83 -0
- package/src/parser/Core/Position.res +54 -0
- package/src/parser/Core/Types.mjs +435 -0
- package/src/parser/Core/Types.res +331 -0
- package/src/parser/Core/__tests__/Bounds_test.mjs +326 -0
- package/src/parser/Core/__tests__/Bounds_test.res +412 -0
- package/src/parser/Core/__tests__/Grid_test.mjs +322 -0
- package/src/parser/Core/__tests__/Grid_test.res +319 -0
- package/src/parser/Core/__tests__/Types_test.mjs +614 -0
- package/src/parser/Core/__tests__/Types_test.res +650 -0
- package/src/parser/Detector/BoxTracer.mjs +302 -0
- package/src/parser/Detector/BoxTracer.res +374 -0
- package/src/parser/Detector/HierarchyBuilder.mjs +158 -0
- package/src/parser/Detector/HierarchyBuilder.res +315 -0
- package/src/parser/Detector/ShapeDetector.mjs +134 -0
- package/src/parser/Detector/ShapeDetector.res +236 -0
- package/src/parser/Detector/__tests__/BoxTracer_test.mjs +70 -0
- package/src/parser/Detector/__tests__/BoxTracer_test.res +92 -0
- package/src/parser/Detector/__tests__/HierarchyBuilder_test.mjs +489 -0
- package/src/parser/Detector/__tests__/HierarchyBuilder_test.res +849 -0
- package/src/parser/Detector/__tests__/ShapeDetector_test.mjs +377 -0
- package/src/parser/Detector/__tests__/ShapeDetector_test.res +563 -0
- package/src/parser/Errors/ErrorContext.mjs +106 -0
- package/src/parser/Errors/ErrorContext.res +191 -0
- package/src/parser/Errors/ErrorMessages.mjs +289 -0
- package/src/parser/Errors/ErrorMessages.res +303 -0
- package/src/parser/Errors/ErrorTypes.mjs +105 -0
- package/src/parser/Errors/ErrorTypes.res +169 -0
- package/src/parser/Interactions/InteractionMerger.mjs +266 -0
- package/src/parser/Interactions/InteractionMerger.res +450 -0
- package/src/parser/Interactions/InteractionParser.mjs +88 -0
- package/src/parser/Interactions/InteractionParser.res +127 -0
- package/src/parser/Interactions/SimpleInteractionParser.mjs +278 -0
- package/src/parser/Interactions/SimpleInteractionParser.res +262 -0
- package/src/parser/Interactions/__tests__/InteractionMerger_test.mjs +576 -0
- package/src/parser/Interactions/__tests__/InteractionMerger_test.res +646 -0
- package/src/parser/Parser.gen.tsx +96 -0
- package/src/parser/Parser.mjs +212 -0
- package/src/parser/Parser.res +481 -0
- package/src/parser/Scanner/__tests__/Grid_manual.mjs +214 -0
- package/src/parser/Scanner/__tests__/Grid_manual.res +141 -0
- package/src/parser/Semantic/ASTBuilder.mjs +197 -0
- package/src/parser/Semantic/ASTBuilder.res +288 -0
- package/src/parser/Semantic/AlignmentCalc.mjs +41 -0
- package/src/parser/Semantic/AlignmentCalc.res +104 -0
- package/src/parser/Semantic/Elements/ButtonParser.mjs +58 -0
- package/src/parser/Semantic/Elements/ButtonParser.res +131 -0
- package/src/parser/Semantic/Elements/CheckboxParser.mjs +58 -0
- package/src/parser/Semantic/Elements/CheckboxParser.res +79 -0
- package/src/parser/Semantic/Elements/CodeTextParser.mjs +50 -0
- package/src/parser/Semantic/Elements/CodeTextParser.res +111 -0
- package/src/parser/Semantic/Elements/ElementParser.mjs +15 -0
- package/src/parser/Semantic/Elements/ElementParser.res +83 -0
- package/src/parser/Semantic/Elements/EmphasisParser.mjs +46 -0
- package/src/parser/Semantic/Elements/EmphasisParser.res +67 -0
- package/src/parser/Semantic/Elements/InputParser.mjs +41 -0
- package/src/parser/Semantic/Elements/InputParser.res +97 -0
- package/src/parser/Semantic/Elements/LinkParser.mjs +60 -0
- package/src/parser/Semantic/Elements/LinkParser.res +156 -0
- package/src/parser/Semantic/Elements/TextParser.mjs +19 -0
- package/src/parser/Semantic/Elements/TextParser.res +42 -0
- package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.mjs +189 -0
- package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.res +257 -0
- package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.mjs +202 -0
- package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.res +250 -0
- package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.mjs +293 -0
- package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.res +134 -0
- package/src/parser/Semantic/Elements/__tests__/InputParser_test.mjs +253 -0
- package/src/parser/Semantic/Elements/__tests__/InputParser_test.res +304 -0
- package/src/parser/Semantic/Elements/__tests__/LinkParser_test.mjs +289 -0
- package/src/parser/Semantic/Elements/__tests__/LinkParser_test.res +402 -0
- package/src/parser/Semantic/Elements/__tests__/TextParser_test.mjs +149 -0
- package/src/parser/Semantic/Elements/__tests__/TextParser_test.res +167 -0
- package/src/parser/Semantic/ParserRegistry.mjs +82 -0
- package/src/parser/Semantic/ParserRegistry.res +145 -0
- package/src/parser/Semantic/SemanticParser.mjs +850 -0
- package/src/parser/Semantic/SemanticParser.res +1368 -0
- package/src/parser/Semantic/__tests__/ASTBuilder_test.mjs +187 -0
- package/src/parser/Semantic/__tests__/ASTBuilder_test.res +192 -0
- package/src/parser/Semantic/__tests__/ParserRegistry_test.mjs +154 -0
- package/src/parser/Semantic/__tests__/ParserRegistry_test.res +191 -0
- package/src/parser/Semantic/__tests__/SemanticParser_integration_test.mjs +768 -0
- package/src/parser/Semantic/__tests__/SemanticParser_integration_test.res +1069 -0
- package/src/parser/Semantic/__tests__/SemanticParser_manual.mjs +1329 -0
- package/src/parser/Semantic/__tests__/SemanticParser_manual.res +544 -0
- package/src/parser/TestMain.mjs +21 -0
- package/src/parser/TestMain.res +14 -0
- package/src/parser/TextExtractor.mjs +179 -0
- package/src/parser/TextExtractor.res +264 -0
- package/src/parser/__tests__/GridScanner_integration.test.mjs +632 -0
- package/src/parser/__tests__/GridScanner_integration.test.res +816 -0
- package/src/parser/__tests__/Performance.test.mjs +244 -0
- package/src/parser/__tests__/Performance.test.res +371 -0
- package/src/parser/__tests__/PerformanceFixtures.mjs +200 -0
- package/src/parser/__tests__/PerformanceFixtures.res +284 -0
- package/src/parser/__tests__/WyreframeParser_integration.test.mjs +770 -0
- package/src/parser/__tests__/WyreframeParser_integration.test.res +1008 -0
- package/src/parser/__tests__/fixtures/alignment-test.txt +9 -0
- package/src/parser/__tests__/fixtures/all-elements.txt +16 -0
- package/src/parser/__tests__/fixtures/login-scene.txt +17 -0
- package/src/parser/__tests__/fixtures/multi-scene.txt +25 -0
- package/src/parser/__tests__/fixtures/nested-boxes.txt +15 -0
- package/src/parser/__tests__/fixtures/simple-box.txt +5 -0
- package/src/parser/__tests__/fixtures/with-dividers.txt +14 -0
- package/src/renderer/Renderer.gen.tsx +32 -0
- package/src/renderer/Renderer.mjs +391 -0
- package/src/renderer/Renderer.res +558 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// WyreframeParser.res
|
|
2
|
+
// Public API for Wyreframe Parser - ReScript Implementation
|
|
3
|
+
// This module provides the main entry points for parsing wireframes and interactions.
|
|
4
|
+
// All functions are exported to TypeScript via GenType annotations.
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Type Aliases for Public API
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse result type - either a successful AST or an array of parse errors.
|
|
12
|
+
* This Result type is compatible with TypeScript through GenType.
|
|
13
|
+
*/
|
|
14
|
+
type parseResult = result<Types.ast, array<ErrorTypes.t>>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Interaction parse result type.
|
|
18
|
+
*/
|
|
19
|
+
type interactionResult = result<array<Types.sceneInteractions>, array<ErrorTypes.t>>
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Type Aliases for Internal Use
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
// Box type from BoxTracer
|
|
26
|
+
type box = BoxTracer.box
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Stage 1: Grid Scanner
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Scan ASCII wireframe input into a 2D grid structure.
|
|
34
|
+
* Normalizes line endings and creates indexed grid.
|
|
35
|
+
*
|
|
36
|
+
* @param wireframe Raw ASCII wireframe string
|
|
37
|
+
* @returns Result containing Grid or errors
|
|
38
|
+
*
|
|
39
|
+
* Requirements: REQ-1, REQ-2 (Grid Scanner)
|
|
40
|
+
*/
|
|
41
|
+
let scanGrid = (wireframe: string): result<Grid.t, array<ErrorTypes.t>> => {
|
|
42
|
+
// Normalize line endings (CRLF -> LF, CR -> LF)
|
|
43
|
+
let normalized = wireframe->String.replaceAll("\r\n", "\n")->String.replaceAll("\r", "\n")
|
|
44
|
+
|
|
45
|
+
// Split into lines
|
|
46
|
+
let lines = normalized->String.split("\n")
|
|
47
|
+
|
|
48
|
+
// TODO: Add validation for unusual spacing (tabs vs spaces)
|
|
49
|
+
// This would generate UnusualSpacing warnings (REQ-19)
|
|
50
|
+
|
|
51
|
+
// Create grid structure
|
|
52
|
+
let grid = Grid.fromLines(lines)
|
|
53
|
+
|
|
54
|
+
Ok(grid)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Stage 2: Shape Detector
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Detect all boxes and dividers in the grid.
|
|
63
|
+
*
|
|
64
|
+
* This function identifies boxes, traces their boundaries, detects dividers,
|
|
65
|
+
* and builds parent-child hierarchy.
|
|
66
|
+
*
|
|
67
|
+
* @param grid The 2D grid from Stage 1
|
|
68
|
+
* @returns Result containing root boxes or errors
|
|
69
|
+
*
|
|
70
|
+
* Requirements: REQ-3, REQ-4, REQ-5, REQ-6, REQ-7 (Shape Detection)
|
|
71
|
+
*/
|
|
72
|
+
let detectShapes = (grid: Grid.t): result<array<box>, array<ErrorTypes.t>> => {
|
|
73
|
+
// Use ShapeDetector to detect all shapes in the grid
|
|
74
|
+
ShapeDetector.detect(grid)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Stage 3: Semantic Parser
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert BoxTracer.box to SemanticParser.box
|
|
83
|
+
* The types are structurally identical except BoxTracer has mutable children
|
|
84
|
+
*/
|
|
85
|
+
let rec convertBox = (tracerBox: box): SemanticParser.box => {
|
|
86
|
+
{
|
|
87
|
+
name: tracerBox.name,
|
|
88
|
+
bounds: tracerBox.bounds,
|
|
89
|
+
children: tracerBox.children->Array.map(convertBox),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse box contents into semantic elements and build AST.
|
|
95
|
+
*
|
|
96
|
+
* This function extracts content, recognizes elements, calculates alignment,
|
|
97
|
+
* parses scene directives, and builds the complete AST.
|
|
98
|
+
*
|
|
99
|
+
* @param grid The 2D grid from Stage 1
|
|
100
|
+
* @param shapes Root boxes from Stage 2
|
|
101
|
+
* @returns Result containing AST or errors
|
|
102
|
+
*
|
|
103
|
+
* Requirements: REQ-8 through REQ-15 (Semantic Parser)
|
|
104
|
+
*/
|
|
105
|
+
let parseSemantics = (
|
|
106
|
+
grid: Grid.t,
|
|
107
|
+
shapes: array<box>,
|
|
108
|
+
): result<Types.ast, array<ErrorTypes.t>> => {
|
|
109
|
+
// Create parser registry with all element parsers
|
|
110
|
+
let registry = ParserRegistry.makeDefault()
|
|
111
|
+
|
|
112
|
+
// Convert BoxTracer boxes to SemanticParser boxes
|
|
113
|
+
let semanticBoxes = shapes->Array.map(convertBox)
|
|
114
|
+
|
|
115
|
+
// Build parse context
|
|
116
|
+
let context: SemanticParser.parseContext = {
|
|
117
|
+
gridCells: grid.cells,
|
|
118
|
+
shapes: semanticBoxes,
|
|
119
|
+
registry: registry,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Parse semantics using SemanticParser
|
|
123
|
+
SemanticParser.parse(context)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Interaction DSL Parser (Optional)
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse interaction DSL and return interaction definitions.
|
|
132
|
+
*
|
|
133
|
+
* @param dsl Interaction DSL string
|
|
134
|
+
* @returns Result containing interactions or errors
|
|
135
|
+
*
|
|
136
|
+
* Requirements: Interaction DSL Parser
|
|
137
|
+
*/
|
|
138
|
+
let parseInteractionsDSL = (
|
|
139
|
+
dsl: string,
|
|
140
|
+
): result<array<Types.sceneInteractions>, array<ErrorTypes.t>> => {
|
|
141
|
+
// Parse interactions using InteractionParser
|
|
142
|
+
switch InteractionParser.parse(dsl) {
|
|
143
|
+
| Ok(interactions) => Ok(interactions)
|
|
144
|
+
| Error({message, position}) => {
|
|
145
|
+
// Convert InteractionParser error to ErrorTypes.t
|
|
146
|
+
let error = ErrorTypes.make(
|
|
147
|
+
InvalidInteractionDSL({
|
|
148
|
+
message: message,
|
|
149
|
+
position: position,
|
|
150
|
+
}),
|
|
151
|
+
None,
|
|
152
|
+
)
|
|
153
|
+
Error([error])
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Merge interaction definitions into the AST.
|
|
160
|
+
*
|
|
161
|
+
* Matches interactions to elements by ID and attaches properties and actions.
|
|
162
|
+
*
|
|
163
|
+
* @param ast Base AST from semantic parsing
|
|
164
|
+
* @param sceneInteractions Interaction definitions grouped by scene
|
|
165
|
+
* @returns AST with interactions merged in
|
|
166
|
+
*
|
|
167
|
+
* Requirements: Integration
|
|
168
|
+
*/
|
|
169
|
+
let mergeInteractionsIntoAST = (
|
|
170
|
+
ast: Types.ast,
|
|
171
|
+
sceneInteractions: array<Types.sceneInteractions>,
|
|
172
|
+
): Types.ast => {
|
|
173
|
+
// Merge interactions using InteractionMerger
|
|
174
|
+
switch InteractionMerger.mergeInteractions(ast, sceneInteractions) {
|
|
175
|
+
| Ok(mergedAst) => mergedAst
|
|
176
|
+
| Error(_errors) => {
|
|
177
|
+
// If merging fails, return AST without interactions
|
|
178
|
+
// Errors are silent - this maintains backward compatibility
|
|
179
|
+
ast
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// Main Public API Functions
|
|
186
|
+
// ============================================================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parse a single scene block through the 3-stage pipeline.
|
|
190
|
+
*
|
|
191
|
+
* @param sceneContent ASCII wireframe content for one scene (without directives)
|
|
192
|
+
* @param sceneMetadata Scene metadata from directives
|
|
193
|
+
* @param errors Accumulator for errors
|
|
194
|
+
* @returns Parsed scene or None if parsing failed
|
|
195
|
+
*/
|
|
196
|
+
let parseSingleScene = (
|
|
197
|
+
sceneContent: string,
|
|
198
|
+
sceneMetadata: SemanticParser.sceneMetadata,
|
|
199
|
+
errors: array<ErrorTypes.t>,
|
|
200
|
+
): option<Types.scene> => {
|
|
201
|
+
// Stage 1: Grid Scanner
|
|
202
|
+
let gridResult = scanGrid(sceneContent)
|
|
203
|
+
|
|
204
|
+
switch gridResult {
|
|
205
|
+
| Error(gridErrors) => {
|
|
206
|
+
gridErrors->Array.forEach(err => errors->Array.push(err)->ignore)
|
|
207
|
+
None
|
|
208
|
+
}
|
|
209
|
+
| Ok(grid) => {
|
|
210
|
+
// Stage 2: Shape Detector
|
|
211
|
+
let shapesResult = detectShapes(grid)
|
|
212
|
+
|
|
213
|
+
let shapes = switch shapesResult {
|
|
214
|
+
| Error(shapeErrors) => {
|
|
215
|
+
shapeErrors->Array.forEach(err => errors->Array.push(err)->ignore)
|
|
216
|
+
[]
|
|
217
|
+
}
|
|
218
|
+
| Ok(shapes) => shapes
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Stage 3: Parse box content into elements
|
|
222
|
+
let registry = ParserRegistry.makeDefault()
|
|
223
|
+
let semanticBoxes = shapes->Array.map(convertBox)
|
|
224
|
+
|
|
225
|
+
// Parse each box recursively
|
|
226
|
+
// parseBoxRecursive returns a Box element with children inside
|
|
227
|
+
// Strategy:
|
|
228
|
+
// - Named boxes (e.g., "Login", "Level1") are semantically meaningful → keep them
|
|
229
|
+
// - Unnamed boxes are just visual containers → flatten to children only
|
|
230
|
+
// This prevents duplication: children are only inside the Box, NOT duplicated at scene level
|
|
231
|
+
let elements = semanticBoxes->Array.flatMap(box => {
|
|
232
|
+
let boxElement = SemanticParser.parseBoxRecursive(box, grid.cells, registry)
|
|
233
|
+
switch boxElement {
|
|
234
|
+
| Box({name: Some(_), _}) as namedBox => [namedBox] // Keep named boxes
|
|
235
|
+
| Box({name: None, children, _}) => children // Flatten unnamed boxes
|
|
236
|
+
| elem => [elem]
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Build scene from metadata and elements
|
|
241
|
+
Some({
|
|
242
|
+
id: sceneMetadata.id,
|
|
243
|
+
title: sceneMetadata.title,
|
|
244
|
+
transition: sceneMetadata.transition,
|
|
245
|
+
device: sceneMetadata.device,
|
|
246
|
+
elements: elements,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Internal parsing function - parses wireframe and optional interactions separately.
|
|
254
|
+
*
|
|
255
|
+
* This executes the complete 3-stage pipeline for each scene:
|
|
256
|
+
* 1. Split wireframe by scene separators ("---")
|
|
257
|
+
* 2. For each scene:
|
|
258
|
+
* a. Parse scene directives (@scene, @title, @transition)
|
|
259
|
+
* b. Grid Scanner - converts ASCII to 2D grid
|
|
260
|
+
* c. Shape Detector - identifies boxes and hierarchy
|
|
261
|
+
* d. Semantic Parser - recognizes elements and builds scene
|
|
262
|
+
* 3. Combine all scenes into AST
|
|
263
|
+
*
|
|
264
|
+
* Optionally parses and merges interaction DSL if provided.
|
|
265
|
+
*
|
|
266
|
+
* Error Handling:
|
|
267
|
+
* - Collects errors from all stages
|
|
268
|
+
* - Returns all errors together (no early stopping)
|
|
269
|
+
* - Continues parsing even after non-fatal errors
|
|
270
|
+
*
|
|
271
|
+
* @param wireframe ASCII wireframe string (may contain multiple scenes)
|
|
272
|
+
* @param interactions Optional interaction DSL string
|
|
273
|
+
* @returns Result containing AST or array of parse errors
|
|
274
|
+
*/
|
|
275
|
+
let parseInternal = (wireframe: string, interactions: option<string>): parseResult => {
|
|
276
|
+
// Accumulator for all errors across stages
|
|
277
|
+
let allErrors = []
|
|
278
|
+
|
|
279
|
+
// Split wireframe into scene blocks
|
|
280
|
+
let sceneBlocks = SemanticParser.splitSceneBlocks(wireframe)
|
|
281
|
+
|
|
282
|
+
// Check if wireframe is empty
|
|
283
|
+
let trimmed = wireframe->String.trim
|
|
284
|
+
if sceneBlocks->Array.length === 0 && trimmed === "" {
|
|
285
|
+
// Empty wireframe - return empty AST
|
|
286
|
+
Ok({scenes: []})
|
|
287
|
+
} else {
|
|
288
|
+
// Parse each scene block
|
|
289
|
+
let scenes = []
|
|
290
|
+
|
|
291
|
+
sceneBlocks->Array.forEach(block => {
|
|
292
|
+
// Parse scene directives
|
|
293
|
+
let lines = block->String.split("\n")
|
|
294
|
+
let (metadata, contentLines) = SemanticParser.parseSceneDirectives(lines)
|
|
295
|
+
|
|
296
|
+
// Rejoin content lines (without directives)
|
|
297
|
+
let sceneContent = contentLines->Array.join("\n")
|
|
298
|
+
|
|
299
|
+
// Parse this scene through 3-stage pipeline
|
|
300
|
+
switch parseSingleScene(sceneContent, metadata, allErrors) {
|
|
301
|
+
| Some(scene) => scenes->Array.push(scene)->ignore
|
|
302
|
+
| None => () // Scene parsing failed, errors already collected
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Build base AST from scenes
|
|
307
|
+
let baseAst: Types.ast = {scenes: scenes}
|
|
308
|
+
|
|
309
|
+
// Optional: Parse and merge interactions
|
|
310
|
+
let finalAst = switch interactions {
|
|
311
|
+
| None => baseAst
|
|
312
|
+
| Some(dsl) => {
|
|
313
|
+
let interactionsResult = parseInteractionsDSL(dsl)
|
|
314
|
+
|
|
315
|
+
switch interactionsResult {
|
|
316
|
+
| Error(errors) => {
|
|
317
|
+
errors->Array.forEach(err => allErrors->Array.push(err)->ignore)
|
|
318
|
+
baseAst // Return AST without interactions on error
|
|
319
|
+
}
|
|
320
|
+
| Ok(sceneInteractions) => {
|
|
321
|
+
// Merge interactions into AST
|
|
322
|
+
mergeInteractionsIntoAST(baseAst, sceneInteractions)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Return final result
|
|
329
|
+
// Return Error if all boxes failed to parse and we have errors
|
|
330
|
+
// Return Ok if at least some elements were parsed successfully
|
|
331
|
+
let totalElements = finalAst.scenes->Array.reduce(0, (acc, scene) => {
|
|
332
|
+
acc + Array.length(scene.elements)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
if Array.length(allErrors) > 0 && totalElements === 0 {
|
|
336
|
+
// No elements parsed and we have errors - return error
|
|
337
|
+
Error(allErrors)
|
|
338
|
+
} else {
|
|
339
|
+
// Either no errors, or some elements were parsed - return Ok
|
|
340
|
+
Ok(finalAst)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Main parsing function - parses mixed text containing wireframe and interactions.
|
|
347
|
+
*
|
|
348
|
+
* This function accepts a single text input that can contain:
|
|
349
|
+
* - ASCII wireframe (boxes with +---+, | |, etc.)
|
|
350
|
+
* - Interaction DSL (#id:, [Button]:, "Link": with properties)
|
|
351
|
+
* - Markdown, comments, or other noise (automatically ignored)
|
|
352
|
+
*
|
|
353
|
+
* The parser intelligently extracts wireframe and interaction content,
|
|
354
|
+
* allowing you to write everything in one place.
|
|
355
|
+
*
|
|
356
|
+
* Example:
|
|
357
|
+
* ```
|
|
358
|
+
* @scene: login
|
|
359
|
+
*
|
|
360
|
+
* +---------------------------+
|
|
361
|
+
* | 'WYREFRAME' |
|
|
362
|
+
* | +---------------------+ |
|
|
363
|
+
* | | #email | |
|
|
364
|
+
* | +---------------------+ |
|
|
365
|
+
* | [ Login ] |
|
|
366
|
+
* +---------------------------+
|
|
367
|
+
*
|
|
368
|
+
* #email:
|
|
369
|
+
* placeholder: "Enter your email"
|
|
370
|
+
*
|
|
371
|
+
* [Login]:
|
|
372
|
+
* @click -> goto(dashboard)
|
|
373
|
+
* ```
|
|
374
|
+
*
|
|
375
|
+
* @param text Mixed text containing wireframe and/or interactions
|
|
376
|
+
* @returns Result containing AST or array of parse errors
|
|
377
|
+
*
|
|
378
|
+
* REQ-20: Backward Compatibility
|
|
379
|
+
* REQ-21: Public API Stability
|
|
380
|
+
* REQ-28: Error Recovery (collect all errors)
|
|
381
|
+
*/
|
|
382
|
+
@genType
|
|
383
|
+
let parse = (text: string): parseResult => {
|
|
384
|
+
// Extract wireframe and interactions from mixed content
|
|
385
|
+
let extracted = TextExtractor.extract(text)
|
|
386
|
+
|
|
387
|
+
// Parse with extracted content
|
|
388
|
+
let interactions = if extracted.interactions->String.trim === "" {
|
|
389
|
+
None
|
|
390
|
+
} else {
|
|
391
|
+
Some(extracted.interactions)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
parseInternal(extracted.wireframe, interactions)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Parse only the wireframe structure (no interactions).
|
|
399
|
+
* Convenience function that calls parseInternal(wireframe, None).
|
|
400
|
+
*
|
|
401
|
+
* @param wireframe ASCII wireframe string
|
|
402
|
+
* @returns Result containing AST or array of parse errors
|
|
403
|
+
*
|
|
404
|
+
* REQ-21: Public API Stability
|
|
405
|
+
*/
|
|
406
|
+
@genType
|
|
407
|
+
let parseWireframe = (wireframe: string): parseResult => {
|
|
408
|
+
parseInternal(wireframe, None)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Parse only the interaction DSL.
|
|
413
|
+
* Useful when interactions are defined separately from the wireframe structure.
|
|
414
|
+
*
|
|
415
|
+
* @param dsl Interaction DSL string
|
|
416
|
+
* @returns Result containing scene interactions or array of parse errors
|
|
417
|
+
*
|
|
418
|
+
* REQ-21: Public API Stability
|
|
419
|
+
*/
|
|
420
|
+
@genType
|
|
421
|
+
let parseInteractions = (dsl: string): interactionResult => {
|
|
422
|
+
parseInteractionsDSL(dsl)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Merge interaction definitions into an existing AST.
|
|
427
|
+
* Attaches properties and actions to elements based on element IDs.
|
|
428
|
+
*
|
|
429
|
+
* @param ast Base AST from wireframe parsing
|
|
430
|
+
* @param sceneInteractions Array of scene interactions to merge
|
|
431
|
+
* @returns AST with interactions merged into elements
|
|
432
|
+
*
|
|
433
|
+
* Integration requirement
|
|
434
|
+
*/
|
|
435
|
+
@genType
|
|
436
|
+
let mergeInteractions = (
|
|
437
|
+
ast: Types.ast,
|
|
438
|
+
_sceneInteractions: array<Types.sceneInteractions>,
|
|
439
|
+
): Types.ast => {
|
|
440
|
+
// TODO: Implement AST merger
|
|
441
|
+
// Match interactions to elements by ID
|
|
442
|
+
// Validate all element IDs exist
|
|
443
|
+
// Attach properties and actions
|
|
444
|
+
|
|
445
|
+
// Placeholder - return unchanged AST
|
|
446
|
+
ast
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ============================================================================
|
|
450
|
+
// Helper Functions (Not exported to TypeScript)
|
|
451
|
+
// ============================================================================
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Collect all errors from multiple parsing stages.
|
|
455
|
+
* Ensures all errors are reported, not just the first one (REQ-28).
|
|
456
|
+
*/
|
|
457
|
+
let collectErrors = (
|
|
458
|
+
gridErrors: array<ErrorTypes.t>,
|
|
459
|
+
shapeErrors: array<ErrorTypes.t>,
|
|
460
|
+
semanticErrors: array<ErrorTypes.t>,
|
|
461
|
+
): array<ErrorTypes.t> => {
|
|
462
|
+
[gridErrors, shapeErrors, semanticErrors]->Array.flatMap(x => x)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ============================================================================
|
|
466
|
+
// Version Information
|
|
467
|
+
// ============================================================================
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Parser version string.
|
|
471
|
+
* Exported to TypeScript for version checking and compatibility validation.
|
|
472
|
+
*/
|
|
473
|
+
@genType
|
|
474
|
+
let version = "0.1.0"
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Parser implementation identifier.
|
|
478
|
+
* Useful for distinguishing between legacy JavaScript and new ReScript parser.
|
|
479
|
+
*/
|
|
480
|
+
@genType
|
|
481
|
+
let implementation = "rescript"
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Grid from "../../Core/Grid.mjs";
|
|
4
|
+
import * as Types from "../../Core/Types.mjs";
|
|
5
|
+
import * as Core__Array from "@rescript/core/src/Core__Array.mjs";
|
|
6
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
7
|
+
|
|
8
|
+
function check(condition, message) {
|
|
9
|
+
if (condition) {
|
|
10
|
+
console.log("✓ PASS: " + message);
|
|
11
|
+
} else {
|
|
12
|
+
console.error("✗ FAIL: " + message);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function assertEqual(actual, expected, message) {
|
|
17
|
+
if (Primitive_object.equal(actual, expected)) {
|
|
18
|
+
console.log("✓ PASS: " + message);
|
|
19
|
+
} else {
|
|
20
|
+
console.error("✗ FAIL: " + message);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log("\n=== Grid Construction Tests ===\n");
|
|
25
|
+
|
|
26
|
+
let lines1 = [
|
|
27
|
+
"abc",
|
|
28
|
+
"def",
|
|
29
|
+
"ghi"
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let grid1 = Grid.fromLines(lines1);
|
|
33
|
+
|
|
34
|
+
assertEqual(grid1.width, 3, "Grid width for uniform lines");
|
|
35
|
+
|
|
36
|
+
assertEqual(grid1.height, 3, "Grid height for uniform lines");
|
|
37
|
+
|
|
38
|
+
let lines2 = [
|
|
39
|
+
"abc",
|
|
40
|
+
"de",
|
|
41
|
+
"f"
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
let grid2 = Grid.fromLines(lines2);
|
|
45
|
+
|
|
46
|
+
assertEqual(grid2.width, 3, "Grid width after normalization");
|
|
47
|
+
|
|
48
|
+
assertEqual(grid2.height, 3, "Grid height after normalization");
|
|
49
|
+
|
|
50
|
+
let lines3 = [
|
|
51
|
+
"+--+",
|
|
52
|
+
"| |",
|
|
53
|
+
"+--+"
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
let grid3 = Grid.fromLines(lines3);
|
|
57
|
+
|
|
58
|
+
assertEqual(grid3.cornerIndex.length, 4, "Corner index count");
|
|
59
|
+
|
|
60
|
+
assertEqual(grid3.hLineIndex.length, 4, "HLine index count");
|
|
61
|
+
|
|
62
|
+
assertEqual(grid3.vLineIndex.length, 2, "VLine index count");
|
|
63
|
+
|
|
64
|
+
console.log("\n=== Character Access Tests ===\n");
|
|
65
|
+
|
|
66
|
+
let match = Grid.get(grid3, Types.Position.make(0, 0));
|
|
67
|
+
|
|
68
|
+
if (match === "Corner" && typeof match !== "object") {
|
|
69
|
+
console.log("✓ PASS: Get character at (0,0) returns Corner");
|
|
70
|
+
} else {
|
|
71
|
+
console.error("✗ FAIL: Get character at (0,0) should return Corner");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let match$1 = Grid.get(grid3, Types.Position.make(10, 10));
|
|
75
|
+
|
|
76
|
+
if (match$1 !== undefined) {
|
|
77
|
+
console.error("✗ FAIL: Get character out of bounds should return None");
|
|
78
|
+
} else {
|
|
79
|
+
console.log("✓ PASS: Get character out of bounds returns None");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let line = Grid.getLine(grid3, 0);
|
|
83
|
+
|
|
84
|
+
if (line !== undefined) {
|
|
85
|
+
assertEqual(line.length, 4, "Get line returns correct length");
|
|
86
|
+
} else {
|
|
87
|
+
console.error("✗ FAIL: Get line should return Some");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log("\n=== Directional Scanning Tests ===\n");
|
|
91
|
+
|
|
92
|
+
let scanResults = Grid.scanRight(grid3, Types.Position.make(0, 0), cell => {
|
|
93
|
+
if (typeof cell === "object") {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
switch (cell) {
|
|
97
|
+
case "Corner" :
|
|
98
|
+
case "HLine" :
|
|
99
|
+
return true;
|
|
100
|
+
default:
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assertEqual(scanResults.length, 4, "Scan right collects correct number of characters");
|
|
106
|
+
|
|
107
|
+
let scanDownResults = Grid.scanDown(grid3, Types.Position.make(0, 0), cell => {
|
|
108
|
+
if (typeof cell === "object") {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
switch (cell) {
|
|
112
|
+
case "Corner" :
|
|
113
|
+
case "VLine" :
|
|
114
|
+
return true;
|
|
115
|
+
default:
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
assertEqual(scanDownResults.length, 3, "Scan down collects correct number of characters");
|
|
121
|
+
|
|
122
|
+
console.log("\n=== Search Operations Tests ===\n");
|
|
123
|
+
|
|
124
|
+
let allCorners = Grid.findAll(grid3, "Corner");
|
|
125
|
+
|
|
126
|
+
assertEqual(allCorners.length, 4, "Find all corners returns 4 positions");
|
|
127
|
+
|
|
128
|
+
let bounds = Types.Bounds.make(0, 0, 1, 2);
|
|
129
|
+
|
|
130
|
+
let cornersInRange = Grid.findInRange(grid3, "Corner", bounds);
|
|
131
|
+
|
|
132
|
+
assertEqual(cornersInRange.length, 1, "Find in range returns corners within bounds");
|
|
133
|
+
|
|
134
|
+
console.log("\n=== Performance Tests ===\n");
|
|
135
|
+
|
|
136
|
+
let largeLines = Core__Array.make(1000, "+".repeat(100));
|
|
137
|
+
|
|
138
|
+
let startTime = Date.now();
|
|
139
|
+
|
|
140
|
+
let largeGrid = Grid.fromLines(largeLines);
|
|
141
|
+
|
|
142
|
+
let endTime = Date.now();
|
|
143
|
+
|
|
144
|
+
let duration = endTime - startTime;
|
|
145
|
+
|
|
146
|
+
assertEqual(largeGrid.height, 1000, "Large grid has correct height");
|
|
147
|
+
|
|
148
|
+
assertEqual(largeGrid.width, 100, "Large grid has correct width");
|
|
149
|
+
|
|
150
|
+
check(duration < 10.0, `Grid construction <10ms (actual: ` + duration.toString() + `ms)`);
|
|
151
|
+
|
|
152
|
+
let findStartTime = Date.now();
|
|
153
|
+
|
|
154
|
+
let _corners = Grid.findAll(largeGrid, "Corner");
|
|
155
|
+
|
|
156
|
+
let findEndTime = Date.now();
|
|
157
|
+
|
|
158
|
+
let findDuration = findEndTime - findStartTime;
|
|
159
|
+
|
|
160
|
+
check(findDuration < 5.0, `Finding all corners using index <5ms (actual: ` + findDuration.toString() + `ms)`);
|
|
161
|
+
|
|
162
|
+
console.log("\n=== Edge Cases Tests ===\n");
|
|
163
|
+
|
|
164
|
+
let singleCharLines = ["+"];
|
|
165
|
+
|
|
166
|
+
let singleCharGrid = Grid.fromLines(singleCharLines);
|
|
167
|
+
|
|
168
|
+
assertEqual(singleCharGrid.width, 1, "Single character grid width");
|
|
169
|
+
|
|
170
|
+
assertEqual(singleCharGrid.height, 1, "Single character grid height");
|
|
171
|
+
|
|
172
|
+
let emptyLines = [
|
|
173
|
+
"",
|
|
174
|
+
"",
|
|
175
|
+
""
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
let emptyGrid = Grid.fromLines(emptyLines);
|
|
179
|
+
|
|
180
|
+
assertEqual(emptyGrid.width, 0, "Empty lines grid width is 0");
|
|
181
|
+
|
|
182
|
+
assertEqual(emptyGrid.height, 3, "Empty lines grid height is 3");
|
|
183
|
+
|
|
184
|
+
console.log("\n=== All Tests Complete ===\n");
|
|
185
|
+
|
|
186
|
+
export {
|
|
187
|
+
check,
|
|
188
|
+
assertEqual,
|
|
189
|
+
lines1,
|
|
190
|
+
grid1,
|
|
191
|
+
lines2,
|
|
192
|
+
grid2,
|
|
193
|
+
lines3,
|
|
194
|
+
grid3,
|
|
195
|
+
scanResults,
|
|
196
|
+
scanDownResults,
|
|
197
|
+
allCorners,
|
|
198
|
+
bounds,
|
|
199
|
+
cornersInRange,
|
|
200
|
+
largeLines,
|
|
201
|
+
startTime,
|
|
202
|
+
largeGrid,
|
|
203
|
+
endTime,
|
|
204
|
+
duration,
|
|
205
|
+
findStartTime,
|
|
206
|
+
_corners,
|
|
207
|
+
findEndTime,
|
|
208
|
+
findDuration,
|
|
209
|
+
singleCharLines,
|
|
210
|
+
singleCharGrid,
|
|
211
|
+
emptyLines,
|
|
212
|
+
emptyGrid,
|
|
213
|
+
}
|
|
214
|
+
/* Not a pure module */
|