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,558 @@
|
|
|
1
|
+
// Renderer.res
|
|
2
|
+
// HTML/DOM Renderer for Wyreframe AST
|
|
3
|
+
// Converts parsed wireframe AST into interactive HTML elements
|
|
4
|
+
|
|
5
|
+
open Types
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// DOM Bindings (must be defined first for type references)
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
module DomBindings = {
|
|
12
|
+
type element
|
|
13
|
+
type document
|
|
14
|
+
type style
|
|
15
|
+
type classList
|
|
16
|
+
type dataset
|
|
17
|
+
|
|
18
|
+
@val external document: document = "document"
|
|
19
|
+
|
|
20
|
+
@send external createElement: (document, string) => element = "createElement"
|
|
21
|
+
@send external getElementById: (document, string) => Nullable.t<element> = "getElementById"
|
|
22
|
+
@send external appendChild: (element, element) => unit = "appendChild"
|
|
23
|
+
@send external querySelector: (element, string) => Nullable.t<element> = "querySelector"
|
|
24
|
+
|
|
25
|
+
@set external setClassName: (element, string) => unit = "className"
|
|
26
|
+
@set external setId: (element, string) => unit = "id"
|
|
27
|
+
@set external setTextContent: (element, string) => unit = "textContent"
|
|
28
|
+
@set external setInnerHTML: (element, string) => unit = "innerHTML"
|
|
29
|
+
@set external setHref: (element, string) => unit = "href"
|
|
30
|
+
@set external setPlaceholder: (element, string) => unit = "placeholder"
|
|
31
|
+
@set external setType: (element, string) => unit = "type"
|
|
32
|
+
@set external setChecked: (element, bool) => unit = "checked"
|
|
33
|
+
|
|
34
|
+
@get external classList: element => classList = "classList"
|
|
35
|
+
@send external add: (classList, string) => unit = "add"
|
|
36
|
+
@send external remove: (classList, string) => unit = "remove"
|
|
37
|
+
@send external contains: (classList, string) => bool = "contains"
|
|
38
|
+
|
|
39
|
+
@get external dataset: element => dataset = "dataset"
|
|
40
|
+
@set_index external setDataAttr: (dataset, string, string) => unit = ""
|
|
41
|
+
|
|
42
|
+
@get external head: document => element = "head"
|
|
43
|
+
@send external addEventListener: (element, string, 'event => unit) => unit = "addEventListener"
|
|
44
|
+
@send external preventDefault: 'event => unit = "preventDefault"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Render Options
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Configuration for the rendering process.
|
|
53
|
+
*/
|
|
54
|
+
type renderOptions = {
|
|
55
|
+
theme: option<string>,
|
|
56
|
+
interactive: bool,
|
|
57
|
+
injectStyles: bool,
|
|
58
|
+
containerClass: option<string>,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Default render options.
|
|
63
|
+
*/
|
|
64
|
+
let defaultOptions: renderOptions = {
|
|
65
|
+
theme: None,
|
|
66
|
+
interactive: true,
|
|
67
|
+
injectStyles: true,
|
|
68
|
+
containerClass: None,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Scene Manager Type
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Scene management interface returned by render function.
|
|
77
|
+
*/
|
|
78
|
+
type sceneManager = {
|
|
79
|
+
goto: string => unit,
|
|
80
|
+
getCurrentScene: unit => option<string>,
|
|
81
|
+
getSceneIds: unit => array<string>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Render result containing the root element and scene manager.
|
|
86
|
+
*/
|
|
87
|
+
type renderResult = {
|
|
88
|
+
root: DomBindings.element,
|
|
89
|
+
sceneManager: sceneManager,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// CSS Styles
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
let defaultStyles = `
|
|
97
|
+
.wf-app { font-family: monospace; position: relative; overflow: hidden; background: #fff; color: #333; font-size: 14px; margin: 0 auto; }
|
|
98
|
+
.wf-app.wf-device-desktop { width: 1440px; height: 900px; max-width: 100%; aspect-ratio: 16/10; }
|
|
99
|
+
.wf-app.wf-device-laptop { width: 1280px; height: 800px; max-width: 100%; aspect-ratio: 16/10; }
|
|
100
|
+
.wf-app.wf-device-tablet { width: 768px; height: 1024px; max-width: 100%; aspect-ratio: 3/4; }
|
|
101
|
+
.wf-app.wf-device-tablet-landscape { width: 1024px; height: 768px; max-width: 100%; aspect-ratio: 4/3; }
|
|
102
|
+
.wf-app.wf-device-mobile { width: 375px; height: 812px; max-width: 100%; aspect-ratio: 375/812; }
|
|
103
|
+
.wf-app.wf-device-mobile-landscape { width: 812px; height: 375px; max-width: 100%; aspect-ratio: 812/375; }
|
|
104
|
+
.wf-scene { position: absolute; top:0; left:0; width:100%; height:100%; padding:16px; box-sizing:border-box; opacity:0; pointer-events:none; transition: opacity 0.3s ease, transform 0.3s ease; overflow-y: auto; }
|
|
105
|
+
.wf-scene.active { opacity:1; pointer-events:auto; }
|
|
106
|
+
.wf-box { border:1px solid #333; padding:12px; margin:8px 0; background:#fff; }
|
|
107
|
+
.wf-box-named { position: relative; margin-top: 16px; }
|
|
108
|
+
.wf-box-named::before { content: attr(data-name); position: absolute; top: -10px; left: 8px; background: #fff; padding: 0 4px; font-size: 12px; color: #666; }
|
|
109
|
+
.wf-row { display:flex; gap:12px; align-items:center; margin:4px 0; }
|
|
110
|
+
.wf-column { flex:1; display:flex; flex-direction:column; gap:4px; }
|
|
111
|
+
.wf-button { display:block; width:fit-content; padding:8px 16px; background:#fff; color:#333; border:1px solid #333; font: inherit; cursor:pointer; margin:4px 0; }
|
|
112
|
+
.wf-button.secondary { background:#eee; }
|
|
113
|
+
.wf-button.ghost { background:transparent; border:1px dashed #999; color:#666; }
|
|
114
|
+
.wf-input { width:100%; padding:8px; border:1px solid #333; font: inherit; box-sizing:border-box; margin:4px 0; }
|
|
115
|
+
.wf-link { display:block; color:#333; text-decoration:underline; cursor:pointer; margin:4px 0; }
|
|
116
|
+
.wf-row .wf-button { display:inline-block; margin:0; }
|
|
117
|
+
.wf-row .wf-link { display:inline; margin:0 8px; }
|
|
118
|
+
.wf-text { margin:4px 0; line-height:1.4; }
|
|
119
|
+
.wf-text.emphasis { font-weight:bold; }
|
|
120
|
+
.wf-divider { border:none; border-top:1px solid #333; margin:12px 0; }
|
|
121
|
+
.wf-section { border:1px solid #333; margin:8px 0; }
|
|
122
|
+
.wf-section-header { background:#fff; padding:4px 8px; font-size:12px; color:#666; border-bottom:1px solid #333; }
|
|
123
|
+
.wf-section-content { padding:8px; }
|
|
124
|
+
.wf-checkbox { display:flex; align-items:center; gap:8px; margin:4px 0; cursor:pointer; }
|
|
125
|
+
.align-center { text-align:center; }
|
|
126
|
+
.align-right { text-align:right; }
|
|
127
|
+
.wf-row.align-center { justify-content:center; }
|
|
128
|
+
.wf-row.align-right { justify-content:flex-end; }
|
|
129
|
+
.wf-button.align-center, .wf-link.align-center { margin-left:auto; margin-right:auto; }
|
|
130
|
+
.wf-button.align-right, .wf-link.align-right { margin-left:auto; margin-right:0; }
|
|
131
|
+
`
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Helper Functions
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
// Noise text filter - filters out box border characters
|
|
138
|
+
let isNoiseText = (content: string): bool => {
|
|
139
|
+
let trimmed = content->String.trim
|
|
140
|
+
if trimmed == "" {
|
|
141
|
+
true
|
|
142
|
+
} else {
|
|
143
|
+
// Box border patterns: +---+, |, ===, etc.
|
|
144
|
+
let borderPattern = %re("/^[+|=\-\s]+$/")
|
|
145
|
+
let hasPipeOrPlus = %re("/[+|]/")
|
|
146
|
+
|
|
147
|
+
Js.Re.test_(borderPattern, trimmed) ||
|
|
148
|
+
Js.Re.test_(hasPipeOrPlus, trimmed)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if a box contains only inputs (should unwrap and render as inputs directly)
|
|
153
|
+
let isInputOnlyBox = (elem: element): bool => {
|
|
154
|
+
switch elem {
|
|
155
|
+
| Box({name: None, children}) =>
|
|
156
|
+
// Check if all children are Inputs (allows for multiple inputs in a box)
|
|
157
|
+
children->Array.length > 0 &&
|
|
158
|
+
children->Array.every(child => {
|
|
159
|
+
switch child {
|
|
160
|
+
| Input(_) => true
|
|
161
|
+
| _ => false
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
| _ => false
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get inputs from a box that contains only inputs
|
|
169
|
+
let getInputsFromBox = (elem: element): array<element> => {
|
|
170
|
+
switch elem {
|
|
171
|
+
| Box({name: None, children}) => children
|
|
172
|
+
| _ => []
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let alignmentToClass = (align: alignment): option<string> => {
|
|
177
|
+
switch align {
|
|
178
|
+
| Left => None
|
|
179
|
+
| Center => Some("align-center")
|
|
180
|
+
| Right => Some("align-right")
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let applyAlignment = (el: DomBindings.element, align: alignment): unit => {
|
|
185
|
+
switch alignmentToClass(align) {
|
|
186
|
+
| Some(cls) => el->DomBindings.classList->DomBindings.add(cls)
|
|
187
|
+
| None => ()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Convert device type to CSS class name.
|
|
193
|
+
*/
|
|
194
|
+
let deviceTypeToClass = (device: deviceType): string => {
|
|
195
|
+
switch device {
|
|
196
|
+
| Desktop => "wf-device-desktop"
|
|
197
|
+
| Laptop => "wf-device-laptop"
|
|
198
|
+
| Tablet => "wf-device-tablet"
|
|
199
|
+
| TabletLandscape => "wf-device-tablet-landscape"
|
|
200
|
+
| Mobile => "wf-device-mobile"
|
|
201
|
+
| MobileLandscape => "wf-device-mobile-landscape"
|
|
202
|
+
| Custom(w, h) => "wf-device-custom-" ++ Int.toString(w) ++ "x" ++ Int.toString(h)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Action Handler Type
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Action handler function type - called when an element's action is triggered.
|
|
212
|
+
* The function receives the action and should execute it (e.g., goto scene).
|
|
213
|
+
*/
|
|
214
|
+
type actionHandler = interactionAction => unit
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Element Rendering
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): option<DomBindings.element> => {
|
|
221
|
+
// Handle input-only boxes by rendering children directly in a wrapper
|
|
222
|
+
if isInputOnlyBox(elem) {
|
|
223
|
+
let inputs = getInputsFromBox(elem)
|
|
224
|
+
// If only one input, render it directly
|
|
225
|
+
switch inputs->Array.get(0) {
|
|
226
|
+
| Some(input) => renderElement(input, ~onAction?)
|
|
227
|
+
| None => None
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
switch elem {
|
|
231
|
+
| Box({name, children, _}) => {
|
|
232
|
+
let div = DomBindings.document->DomBindings.createElement("div")
|
|
233
|
+
div->DomBindings.setClassName("wf-box")
|
|
234
|
+
|
|
235
|
+
switch name {
|
|
236
|
+
| Some(n) => {
|
|
237
|
+
div->DomBindings.classList->DomBindings.add("wf-box-named")
|
|
238
|
+
div->DomBindings.dataset->DomBindings.setDataAttr("name", n)
|
|
239
|
+
}
|
|
240
|
+
| None => ()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
children->Array.forEach(child => {
|
|
244
|
+
switch renderElement(child, ~onAction?) {
|
|
245
|
+
| Some(el) => div->DomBindings.appendChild(el)
|
|
246
|
+
| None => ()
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
Some(div)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
| Button({id, text, align, actions, _}) => {
|
|
254
|
+
let btn = DomBindings.document->DomBindings.createElement("button")
|
|
255
|
+
btn->DomBindings.setClassName("wf-button")
|
|
256
|
+
btn->DomBindings.setId(id)
|
|
257
|
+
btn->DomBindings.setTextContent(text)
|
|
258
|
+
applyAlignment(btn, align)
|
|
259
|
+
|
|
260
|
+
// Attach click handler for actions
|
|
261
|
+
if actions->Array.length > 0 {
|
|
262
|
+
switch onAction {
|
|
263
|
+
| Some(handler) => {
|
|
264
|
+
btn->DomBindings.addEventListener("click", _event => {
|
|
265
|
+
// Execute first action (most common case)
|
|
266
|
+
switch actions->Array.get(0) {
|
|
267
|
+
| Some(action) => handler(action)
|
|
268
|
+
| None => ()
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
| None => ()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
Some(btn)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
| Input({id, placeholder, _}) => {
|
|
280
|
+
let input = DomBindings.document->DomBindings.createElement("input")
|
|
281
|
+
input->DomBindings.setClassName("wf-input")
|
|
282
|
+
input->DomBindings.setId(id)
|
|
283
|
+
switch placeholder {
|
|
284
|
+
| Some(p) => input->DomBindings.setPlaceholder(p)
|
|
285
|
+
| None => ()
|
|
286
|
+
}
|
|
287
|
+
Some(input)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
| Link({id, text, align, actions, _}) => {
|
|
291
|
+
let link = DomBindings.document->DomBindings.createElement("a")
|
|
292
|
+
link->DomBindings.setClassName("wf-link")
|
|
293
|
+
link->DomBindings.setId(id)
|
|
294
|
+
link->DomBindings.setHref("#")
|
|
295
|
+
link->DomBindings.setTextContent(text)
|
|
296
|
+
applyAlignment(link, align)
|
|
297
|
+
|
|
298
|
+
// Attach click handler for actions
|
|
299
|
+
if actions->Array.length > 0 {
|
|
300
|
+
switch onAction {
|
|
301
|
+
| Some(handler) => {
|
|
302
|
+
link->DomBindings.addEventListener("click", event => {
|
|
303
|
+
DomBindings.preventDefault(event)
|
|
304
|
+
// Execute first action
|
|
305
|
+
switch actions->Array.get(0) {
|
|
306
|
+
| Some(action) => handler(action)
|
|
307
|
+
| None => ()
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
| None => ()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
Some(link)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
| Checkbox({checked, label, _}) => {
|
|
319
|
+
let labelEl = DomBindings.document->DomBindings.createElement("label")
|
|
320
|
+
labelEl->DomBindings.setClassName("wf-checkbox")
|
|
321
|
+
|
|
322
|
+
let input = DomBindings.document->DomBindings.createElement("input")
|
|
323
|
+
input->DomBindings.setType("checkbox")
|
|
324
|
+
input->DomBindings.setChecked(checked)
|
|
325
|
+
|
|
326
|
+
let span = DomBindings.document->DomBindings.createElement("span")
|
|
327
|
+
span->DomBindings.setTextContent(label)
|
|
328
|
+
|
|
329
|
+
labelEl->DomBindings.appendChild(input)
|
|
330
|
+
labelEl->DomBindings.appendChild(span)
|
|
331
|
+
Some(labelEl)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
| Text({content, emphasis, align, _}) => {
|
|
335
|
+
// Filter noise text (box borders, etc.)
|
|
336
|
+
if isNoiseText(content) {
|
|
337
|
+
None
|
|
338
|
+
} else {
|
|
339
|
+
let p = DomBindings.document->DomBindings.createElement("p")
|
|
340
|
+
p->DomBindings.setClassName("wf-text")
|
|
341
|
+
if emphasis {
|
|
342
|
+
p->DomBindings.classList->DomBindings.add("emphasis")
|
|
343
|
+
}
|
|
344
|
+
applyAlignment(p, align)
|
|
345
|
+
p->DomBindings.setTextContent(content)
|
|
346
|
+
Some(p)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
| Divider(_) => {
|
|
351
|
+
let hr = DomBindings.document->DomBindings.createElement("hr")
|
|
352
|
+
hr->DomBindings.setClassName("wf-divider")
|
|
353
|
+
Some(hr)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
| Row({children, align}) => {
|
|
357
|
+
let row = DomBindings.document->DomBindings.createElement("div")
|
|
358
|
+
row->DomBindings.setClassName("wf-row")
|
|
359
|
+
applyAlignment(row, align)
|
|
360
|
+
|
|
361
|
+
children->Array.forEach(child => {
|
|
362
|
+
switch renderElement(child, ~onAction?) {
|
|
363
|
+
| Some(el) => row->DomBindings.appendChild(el)
|
|
364
|
+
| None => ()
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
Some(row)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
| Section({name, children}) => {
|
|
372
|
+
let section = DomBindings.document->DomBindings.createElement("div")
|
|
373
|
+
section->DomBindings.setClassName("wf-section")
|
|
374
|
+
|
|
375
|
+
let header = DomBindings.document->DomBindings.createElement("div")
|
|
376
|
+
header->DomBindings.setClassName("wf-section-header")
|
|
377
|
+
header->DomBindings.setTextContent(name)
|
|
378
|
+
|
|
379
|
+
let contentEl = DomBindings.document->DomBindings.createElement("div")
|
|
380
|
+
contentEl->DomBindings.setClassName("wf-section-content")
|
|
381
|
+
|
|
382
|
+
children->Array.forEach(child => {
|
|
383
|
+
switch renderElement(child, ~onAction?) {
|
|
384
|
+
| Some(el) => contentEl->DomBindings.appendChild(el)
|
|
385
|
+
| None => ()
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
section->DomBindings.appendChild(header)
|
|
390
|
+
section->DomBindings.appendChild(contentEl)
|
|
391
|
+
Some(section)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let renderScene = (scene: scene, ~onAction: option<actionHandler>=?): DomBindings.element => {
|
|
398
|
+
let sceneEl = DomBindings.document->DomBindings.createElement("div")
|
|
399
|
+
sceneEl->DomBindings.setClassName("wf-scene")
|
|
400
|
+
sceneEl->DomBindings.dataset->DomBindings.setDataAttr("scene", scene.id)
|
|
401
|
+
|
|
402
|
+
scene.elements->Array.forEach(elem => {
|
|
403
|
+
switch renderElement(elem, ~onAction?) {
|
|
404
|
+
| Some(el) => sceneEl->DomBindings.appendChild(el)
|
|
405
|
+
| None => ()
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
sceneEl
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// Scene Manager Implementation
|
|
414
|
+
// ============================================================================
|
|
415
|
+
|
|
416
|
+
let createSceneManager = (scenes: Map.t<string, DomBindings.element>): sceneManager => {
|
|
417
|
+
let currentScene = ref(None)
|
|
418
|
+
|
|
419
|
+
let goto = (id: string): unit => {
|
|
420
|
+
switch currentScene.contents {
|
|
421
|
+
| Some(currentId) => {
|
|
422
|
+
switch scenes->Map.get(currentId) {
|
|
423
|
+
| Some(el) => el->DomBindings.classList->DomBindings.remove("active")
|
|
424
|
+
| None => ()
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
| None => ()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
switch scenes->Map.get(id) {
|
|
431
|
+
| Some(el) => {
|
|
432
|
+
el->DomBindings.classList->DomBindings.add("active")
|
|
433
|
+
currentScene := Some(id)
|
|
434
|
+
}
|
|
435
|
+
| None => ()
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let getCurrentScene = (): option<string> => currentScene.contents
|
|
440
|
+
|
|
441
|
+
let getSceneIds = (): array<string> => {
|
|
442
|
+
scenes->Map.keys->Iterator.toArray
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
{
|
|
446
|
+
goto,
|
|
447
|
+
getCurrentScene,
|
|
448
|
+
getSceneIds,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============================================================================
|
|
453
|
+
// Style Injection
|
|
454
|
+
// ============================================================================
|
|
455
|
+
|
|
456
|
+
let injectStyles = (): unit => {
|
|
457
|
+
switch DomBindings.document->DomBindings.getElementById("wf-styles")->Nullable.toOption {
|
|
458
|
+
| Some(_) => ()
|
|
459
|
+
| None => {
|
|
460
|
+
let style = DomBindings.document->DomBindings.createElement("style")
|
|
461
|
+
style->DomBindings.setId("wf-styles")
|
|
462
|
+
style->DomBindings.setTextContent(defaultStyles)
|
|
463
|
+
DomBindings.document->DomBindings.head->DomBindings.appendChild(style)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ============================================================================
|
|
469
|
+
// Main Render Function
|
|
470
|
+
// ============================================================================
|
|
471
|
+
|
|
472
|
+
@genType
|
|
473
|
+
let render = (ast: ast, options: option<renderOptions>): renderResult => {
|
|
474
|
+
let opts = options->Option.getOr(defaultOptions)
|
|
475
|
+
|
|
476
|
+
if opts.injectStyles {
|
|
477
|
+
injectStyles()
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let app = DomBindings.document->DomBindings.createElement("div")
|
|
481
|
+
app->DomBindings.setClassName("wf-app")
|
|
482
|
+
|
|
483
|
+
switch opts.containerClass {
|
|
484
|
+
| Some(cls) => app->DomBindings.classList->DomBindings.add(cls)
|
|
485
|
+
| None => ()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Apply device class based on first scene's device type
|
|
489
|
+
switch ast.scenes->Array.get(0) {
|
|
490
|
+
| Some(firstScene) => {
|
|
491
|
+
let deviceClass = deviceTypeToClass(firstScene.device)
|
|
492
|
+
app->DomBindings.classList->DomBindings.add(deviceClass)
|
|
493
|
+
}
|
|
494
|
+
| None => ()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Create a ref to hold the goto function (set after sceneManager is created)
|
|
498
|
+
let gotoRef: ref<option<string => unit>> = ref(None)
|
|
499
|
+
|
|
500
|
+
// Create action handler that uses the gotoRef
|
|
501
|
+
let handleAction = (action: interactionAction): unit => {
|
|
502
|
+
switch action {
|
|
503
|
+
| Goto({target, _}) => {
|
|
504
|
+
switch gotoRef.contents {
|
|
505
|
+
| Some(goto) => goto(target)
|
|
506
|
+
| None => ()
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
| Back => {
|
|
510
|
+
// TODO: Implement history-based back navigation
|
|
511
|
+
()
|
|
512
|
+
}
|
|
513
|
+
| Forward => {
|
|
514
|
+
// TODO: Implement history-based forward navigation
|
|
515
|
+
()
|
|
516
|
+
}
|
|
517
|
+
| Validate(_) => {
|
|
518
|
+
// TODO: Implement field validation
|
|
519
|
+
()
|
|
520
|
+
}
|
|
521
|
+
| Call(_) => {
|
|
522
|
+
// TODO: Implement custom function calls
|
|
523
|
+
()
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
let sceneMap = Map.make()
|
|
529
|
+
|
|
530
|
+
ast.scenes->Array.forEach(scene => {
|
|
531
|
+
let sceneEl = renderScene(scene, ~onAction=handleAction)
|
|
532
|
+
app->DomBindings.appendChild(sceneEl)
|
|
533
|
+
sceneMap->Map.set(scene.id, sceneEl)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
let manager = createSceneManager(sceneMap)
|
|
537
|
+
|
|
538
|
+
// Now that sceneManager is created, set the gotoRef
|
|
539
|
+
gotoRef := Some(manager.goto)
|
|
540
|
+
|
|
541
|
+
if ast.scenes->Array.length > 0 {
|
|
542
|
+
switch ast.scenes->Array.get(0) {
|
|
543
|
+
| Some(firstScene) => manager.goto(firstScene.id)
|
|
544
|
+
| None => ()
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
{
|
|
549
|
+
root: app,
|
|
550
|
+
sceneManager: manager,
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
@genType
|
|
555
|
+
let toHTMLString = (_ast: ast, _options: option<renderOptions>): string => {
|
|
556
|
+
"<!-- Static HTML generation not yet implemented -->"
|
|
557
|
+
}
|
|
558
|
+
|