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,104 @@
|
|
|
1
|
+
// AlignmentCalc.res
|
|
2
|
+
// Alignment calculation module for determining element positioning within boxes
|
|
3
|
+
|
|
4
|
+
open Types
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculates the horizontal alignment of an element based on its position
|
|
8
|
+
* within a box's boundaries.
|
|
9
|
+
*
|
|
10
|
+
* Algorithm:
|
|
11
|
+
* 1. Calculate the space to the left and right of the content
|
|
12
|
+
* 2. Convert these to ratios relative to the box interior width
|
|
13
|
+
* 3. Apply threshold rules to determine alignment:
|
|
14
|
+
* - Left: Element is close to left edge (leftRatio < 0.2 && rightRatio > 0.3)
|
|
15
|
+
* - Right: Element is close to right edge (rightRatio < 0.2 && leftRatio > 0.3)
|
|
16
|
+
* - Center: Element is roughly centered (abs(leftRatio - rightRatio) < 0.15)
|
|
17
|
+
* - Default to Left if no conditions match
|
|
18
|
+
*
|
|
19
|
+
* @param content The text content of the element
|
|
20
|
+
* @param position The grid position where the content starts
|
|
21
|
+
* @param boxBounds The bounding box containing the element
|
|
22
|
+
* @return alignment The calculated alignment (Left, Center, or Right)
|
|
23
|
+
*/
|
|
24
|
+
let calculate = (
|
|
25
|
+
content: string,
|
|
26
|
+
position: Position.t,
|
|
27
|
+
boxBounds: Bounds.t,
|
|
28
|
+
): Types.alignment => {
|
|
29
|
+
// Trim content to get actual content length
|
|
30
|
+
let trimmed = content->String.trim
|
|
31
|
+
let contentStart = position.col
|
|
32
|
+
let contentEnd = contentStart + String.length(trimmed)
|
|
33
|
+
|
|
34
|
+
// Box interior bounds (excluding border characters '|' on both sides)
|
|
35
|
+
// boxBounds.left is the column of the left '|', so interior starts at left + 1
|
|
36
|
+
// boxBounds.right is the column of the right '|', so interior ends at right - 1
|
|
37
|
+
let boxLeft = boxBounds.left + 1
|
|
38
|
+
let boxRight = boxBounds.right - 1
|
|
39
|
+
let boxWidth = boxRight - boxLeft
|
|
40
|
+
|
|
41
|
+
// Handle edge case: box is too narrow (width <= 0)
|
|
42
|
+
if boxWidth <= 0 {
|
|
43
|
+
Left
|
|
44
|
+
} else {
|
|
45
|
+
// Calculate space on each side
|
|
46
|
+
let leftSpace = contentStart - boxLeft
|
|
47
|
+
let rightSpace = boxRight - contentEnd
|
|
48
|
+
|
|
49
|
+
// Convert to ratios (0.0 to 1.0) relative to box width
|
|
50
|
+
let leftRatio = Int.toFloat(leftSpace) /. Int.toFloat(boxWidth)
|
|
51
|
+
let rightRatio = Int.toFloat(rightSpace) /. Int.toFloat(boxWidth)
|
|
52
|
+
|
|
53
|
+
// Thresholds for alignment detection
|
|
54
|
+
let leftThreshold = 0.2
|
|
55
|
+
let rightThreshold = 0.2
|
|
56
|
+
let centerTolerance = 0.15
|
|
57
|
+
|
|
58
|
+
// Apply alignment rules
|
|
59
|
+
if leftRatio < leftThreshold && rightRatio > 0.3 {
|
|
60
|
+
// Element is close to left edge with significant space on right
|
|
61
|
+
Left
|
|
62
|
+
} else if rightRatio < rightThreshold && leftRatio > 0.3 {
|
|
63
|
+
// Element is close to right edge with significant space on left
|
|
64
|
+
Right
|
|
65
|
+
} else if Math.abs(leftRatio -. rightRatio) < centerTolerance {
|
|
66
|
+
// Element is roughly centered (ratios are similar)
|
|
67
|
+
Center
|
|
68
|
+
} else {
|
|
69
|
+
// Default to left alignment
|
|
70
|
+
Left
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Alignment strategy determines how alignment should be calculated.
|
|
77
|
+
*
|
|
78
|
+
* - RespectPosition: Use position-based alignment calculation (buttons, links, emphasis)
|
|
79
|
+
* - AlwaysLeft: Always return Left alignment regardless of position (text, checkboxes, inputs)
|
|
80
|
+
*/
|
|
81
|
+
type alignmentStrategy =
|
|
82
|
+
| RespectPosition // Buttons, links, emphasis text
|
|
83
|
+
| AlwaysLeft // Regular text, checkboxes, inputs
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Calculates alignment with a specific strategy.
|
|
87
|
+
*
|
|
88
|
+
* @param content The text content of the element
|
|
89
|
+
* @param position The grid position where the content starts
|
|
90
|
+
* @param boxBounds The bounding box containing the element
|
|
91
|
+
* @param strategy The alignment strategy to use
|
|
92
|
+
* @return alignment The calculated alignment
|
|
93
|
+
*/
|
|
94
|
+
let calculateWithStrategy = (
|
|
95
|
+
content: string,
|
|
96
|
+
position: Position.t,
|
|
97
|
+
boxBounds: Bounds.t,
|
|
98
|
+
strategy: alignmentStrategy,
|
|
99
|
+
): Types.alignment => {
|
|
100
|
+
switch strategy {
|
|
101
|
+
| RespectPosition => calculate(content, position, boxBounds)
|
|
102
|
+
| AlwaysLeft => Left
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Types from "../../Core/Types.mjs";
|
|
4
|
+
import * as AlignmentCalc from "../AlignmentCalc.mjs";
|
|
5
|
+
import * as ElementParser from "./ElementParser.mjs";
|
|
6
|
+
|
|
7
|
+
function slugify(text) {
|
|
8
|
+
return text.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let buttonPattern = /^\s*\[\s*([^\]]+?)\s*\]\s*$/;
|
|
12
|
+
|
|
13
|
+
let quickTestPattern = /\[.*\]/;
|
|
14
|
+
|
|
15
|
+
function canParse(content) {
|
|
16
|
+
return quickTestPattern.test(content);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parse(content, position, bounds) {
|
|
20
|
+
let result = buttonPattern.exec(content);
|
|
21
|
+
if (result == null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let matches = result.slice(1);
|
|
25
|
+
let buttonText = matches[0];
|
|
26
|
+
if (buttonText === undefined) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let buttonText$1 = buttonText.trim();
|
|
30
|
+
let lowerText = buttonText$1.toLowerCase();
|
|
31
|
+
if (buttonText$1 === "" || lowerText === "x") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let buttonId = slugify(buttonText$1);
|
|
35
|
+
let align = AlignmentCalc.calculateWithStrategy(content, position, bounds, "RespectPosition");
|
|
36
|
+
return {
|
|
37
|
+
TAG: "Button",
|
|
38
|
+
id: buttonId,
|
|
39
|
+
text: buttonText$1,
|
|
40
|
+
position: Types.Position.make(position.row, position.col),
|
|
41
|
+
align: align,
|
|
42
|
+
actions: []
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function make() {
|
|
47
|
+
return ElementParser.make(100, canParse, parse);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
slugify,
|
|
52
|
+
buttonPattern,
|
|
53
|
+
quickTestPattern,
|
|
54
|
+
canParse,
|
|
55
|
+
parse,
|
|
56
|
+
make,
|
|
57
|
+
}
|
|
58
|
+
/* Types Not a pure module */
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ButtonParser.res
|
|
2
|
+
|
|
3
|
+
open Types
|
|
4
|
+
// Parser for button syntax: [ Text ]
|
|
5
|
+
//
|
|
6
|
+
// Recognizes button elements in wireframes and extracts their text content.
|
|
7
|
+
// Buttons are defined with square brackets containing text: [ Submit ], [ Cancel ], etc.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Slugify a string to create a valid identifier.
|
|
11
|
+
* Converts text to lowercase, replaces spaces and special chars with hyphens.
|
|
12
|
+
*
|
|
13
|
+
* Examples:
|
|
14
|
+
* - "Submit Form" -> "submit-form"
|
|
15
|
+
* - "Log In" -> "log-in"
|
|
16
|
+
* - "Create Account!" -> "create-account"
|
|
17
|
+
*
|
|
18
|
+
* @param text - The text to slugify
|
|
19
|
+
* @return A slugified identifier string
|
|
20
|
+
*/
|
|
21
|
+
let slugify = (text: string): string => {
|
|
22
|
+
text
|
|
23
|
+
->String.trim
|
|
24
|
+
->String.toLowerCase
|
|
25
|
+
// Replace spaces with hyphens
|
|
26
|
+
->Js.String2.replaceByRe(%re("/\s+/g"), "-")
|
|
27
|
+
// Remove non-alphanumeric characters (except hyphens)
|
|
28
|
+
->Js.String2.replaceByRe(%re("/[^a-z0-9-]/g"), "")
|
|
29
|
+
// Remove consecutive hyphens
|
|
30
|
+
->Js.String2.replaceByRe(%re("/-+/g"), "-")
|
|
31
|
+
// Remove leading/trailing hyphens
|
|
32
|
+
->Js.String2.replaceByRe(%re("/^-+|-+$/g"), "")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Button pattern regex: \[\s*[^\]]+\s*\]
|
|
37
|
+
* Matches: [ Text ] with optional whitespace
|
|
38
|
+
* Examples:
|
|
39
|
+
* - [ Submit ]
|
|
40
|
+
* - [Login]
|
|
41
|
+
* - [ Create Account ]
|
|
42
|
+
*/
|
|
43
|
+
let buttonPattern = %re("/^\s*\[\s*([^\]]+?)\s*\]\s*$/")
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Quick test pattern for canParse (optimized for speed).
|
|
47
|
+
* Just checks if content contains [ and ]
|
|
48
|
+
*/
|
|
49
|
+
let quickTestPattern = %re("/\[.*\]/")
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if content matches button syntax pattern.
|
|
53
|
+
*
|
|
54
|
+
* @param content - The text content to test
|
|
55
|
+
* @return true if content looks like a button
|
|
56
|
+
*/
|
|
57
|
+
let canParse = (content: string): bool => {
|
|
58
|
+
quickTestPattern->RegExp.test(content)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse button element from content.
|
|
63
|
+
*
|
|
64
|
+
* Extracts button text from [ Text ] syntax and creates a Button element.
|
|
65
|
+
* Returns None if:
|
|
66
|
+
* - Pattern doesn't match
|
|
67
|
+
* - Button text is empty after trimming
|
|
68
|
+
*
|
|
69
|
+
* @param content - The text content containing button syntax
|
|
70
|
+
* @param position - Grid position where button appears
|
|
71
|
+
* @param bounds - Bounding box of containing box (for alignment calculation)
|
|
72
|
+
* @return Some(Button element) or None
|
|
73
|
+
*/
|
|
74
|
+
let parse = (
|
|
75
|
+
content: string,
|
|
76
|
+
position: Position.t,
|
|
77
|
+
bounds: Bounds.t,
|
|
78
|
+
): option<Types.element> => {
|
|
79
|
+
switch buttonPattern->RegExp.exec(content) {
|
|
80
|
+
| None => None
|
|
81
|
+
| Some(result) => {
|
|
82
|
+
let matches = result->RegExp.Result.matches
|
|
83
|
+
// RegExp.Result.matches slices off the full match, so matches[0] is the first captured group
|
|
84
|
+
switch matches[0] {
|
|
85
|
+
| None => None
|
|
86
|
+
| Some(buttonText) => {
|
|
87
|
+
let buttonText = buttonText->String.trim
|
|
88
|
+
|
|
89
|
+
// Return None for empty button text or single character 'x' (checkbox pattern)
|
|
90
|
+
// This allows [x] to be handled by CheckboxParser instead
|
|
91
|
+
let lowerText = buttonText->String.toLowerCase
|
|
92
|
+
if buttonText === "" || lowerText === "x" {
|
|
93
|
+
None
|
|
94
|
+
} else {
|
|
95
|
+
let buttonId = slugify(buttonText)
|
|
96
|
+
|
|
97
|
+
// Calculate alignment based on position within bounds
|
|
98
|
+
let align = AlignmentCalc.calculateWithStrategy(
|
|
99
|
+
content,
|
|
100
|
+
position,
|
|
101
|
+
bounds,
|
|
102
|
+
AlignmentCalc.RespectPosition,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
// Create Button element
|
|
106
|
+
Some(
|
|
107
|
+
Types.Button({
|
|
108
|
+
id: buttonId,
|
|
109
|
+
text: buttonText,
|
|
110
|
+
position: Types.Position.make(position.row, position.col),
|
|
111
|
+
align: align,
|
|
112
|
+
actions: [],
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create a ButtonParser instance.
|
|
124
|
+
*
|
|
125
|
+
* Priority: 100 (high priority, checked early in the registry)
|
|
126
|
+
*
|
|
127
|
+
* @return An ElementParser configured for button recognition
|
|
128
|
+
*/
|
|
129
|
+
let make = (): ElementParser.elementParser => {
|
|
130
|
+
ElementParser.make(~priority=100, ~canParse, ~parse)
|
|
131
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Types from "../../Core/Types.mjs";
|
|
4
|
+
import * as ElementParser from "./ElementParser.mjs";
|
|
5
|
+
|
|
6
|
+
let checkedTestPattern = /\[x\]/i;
|
|
7
|
+
|
|
8
|
+
let uncheckedTestPattern = /\[\s*\]/;
|
|
9
|
+
|
|
10
|
+
let checkedParsePattern = /^\[x\]\s*(.*)/i;
|
|
11
|
+
|
|
12
|
+
let uncheckedParsePattern = /^\[\s*\]\s*(.*)/;
|
|
13
|
+
|
|
14
|
+
function make() {
|
|
15
|
+
return ElementParser.make(85, content => {
|
|
16
|
+
if (checkedTestPattern.test(content)) {
|
|
17
|
+
return true;
|
|
18
|
+
} else {
|
|
19
|
+
return uncheckedTestPattern.test(content);
|
|
20
|
+
}
|
|
21
|
+
}, (content, position, _bounds) => {
|
|
22
|
+
let trimmed = content.trim();
|
|
23
|
+
let result = checkedParsePattern.exec(trimmed);
|
|
24
|
+
if (result == null) {
|
|
25
|
+
let result$1 = uncheckedParsePattern.exec(trimmed);
|
|
26
|
+
if (result$1 == null) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let matches = result$1.slice(1);
|
|
30
|
+
let labelStr = matches[0];
|
|
31
|
+
let label = labelStr !== undefined ? labelStr.trim() : "";
|
|
32
|
+
return {
|
|
33
|
+
TAG: "Checkbox",
|
|
34
|
+
checked: false,
|
|
35
|
+
label: label,
|
|
36
|
+
position: Types.Position.make(position.row, position.col)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
let matches$1 = result.slice(1);
|
|
40
|
+
let labelStr$1 = matches$1[0];
|
|
41
|
+
let label$1 = labelStr$1 !== undefined ? labelStr$1.trim() : "";
|
|
42
|
+
return {
|
|
43
|
+
TAG: "Checkbox",
|
|
44
|
+
checked: true,
|
|
45
|
+
label: label$1,
|
|
46
|
+
position: Types.Position.make(position.row, position.col)
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
checkedTestPattern,
|
|
53
|
+
uncheckedTestPattern,
|
|
54
|
+
checkedParsePattern,
|
|
55
|
+
uncheckedParsePattern,
|
|
56
|
+
make,
|
|
57
|
+
}
|
|
58
|
+
/* Types Not a pure module */
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// CheckboxParser.res
|
|
2
|
+
// Parser for checkbox syntax "[x]" (checked) and "[ ]" (unchecked)
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a checkbox element parser.
|
|
6
|
+
*
|
|
7
|
+
* Recognizes:
|
|
8
|
+
* - "[x]" - Checked checkbox
|
|
9
|
+
* - "[ ]" - Unchecked checkbox
|
|
10
|
+
*
|
|
11
|
+
* Optionally followed by label text on the same line.
|
|
12
|
+
*
|
|
13
|
+
* Priority: 85 (high - should be checked before text parser but after buttons)
|
|
14
|
+
*
|
|
15
|
+
* Examples:
|
|
16
|
+
* - "[x] Accept terms" -> Checkbox(checked=true, label="Accept terms")
|
|
17
|
+
* - "[ ] Subscribe" -> Checkbox(checked=false, label="Subscribe")
|
|
18
|
+
* - "[x]" -> Checkbox(checked=true, label="")
|
|
19
|
+
*/
|
|
20
|
+
// Pattern for detecting checkboxes (used in canParse)
|
|
21
|
+
let checkedTestPattern = %re("/\[x\]/i")
|
|
22
|
+
let uncheckedTestPattern = %re("/\[\s*\]/")
|
|
23
|
+
|
|
24
|
+
// Patterns for parsing (capturing label)
|
|
25
|
+
let checkedParsePattern = %re("/^\[x\]\s*(.*)/i")
|
|
26
|
+
let uncheckedParsePattern = %re("/^\[\s*\]\s*(.*)/")
|
|
27
|
+
|
|
28
|
+
let make = (): ElementParser.elementParser => {
|
|
29
|
+
ElementParser.make(
|
|
30
|
+
~priority=85,
|
|
31
|
+
~canParse=content => {
|
|
32
|
+
// Match either [x] or [ ] patterns
|
|
33
|
+
checkedTestPattern->RegExp.test(content) || uncheckedTestPattern->RegExp.test(content)
|
|
34
|
+
},
|
|
35
|
+
~parse=(content, position, _bounds) => {
|
|
36
|
+
let trimmed = content->String.trim
|
|
37
|
+
|
|
38
|
+
// Try to match checked checkbox [x]
|
|
39
|
+
switch checkedParsePattern->RegExp.exec(trimmed) {
|
|
40
|
+
| Some(result) => {
|
|
41
|
+
let matches = result->RegExp.Result.matches
|
|
42
|
+
let label = switch matches[0] {
|
|
43
|
+
| Some(labelStr) => labelStr->String.trim
|
|
44
|
+
| None => ""
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Some(
|
|
48
|
+
Types.Checkbox({
|
|
49
|
+
checked: true,
|
|
50
|
+
label: label,
|
|
51
|
+
position: Types.Position.make(position.row, position.col),
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
| None => {
|
|
56
|
+
// Try to match unchecked checkbox [ ]
|
|
57
|
+
switch uncheckedParsePattern->RegExp.exec(trimmed) {
|
|
58
|
+
| Some(result) => {
|
|
59
|
+
let matches = result->RegExp.Result.matches
|
|
60
|
+
let label = switch matches[0] {
|
|
61
|
+
| Some(labelStr) => labelStr->String.trim
|
|
62
|
+
| None => ""
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Some(
|
|
66
|
+
Types.Checkbox({
|
|
67
|
+
checked: false,
|
|
68
|
+
label: label,
|
|
69
|
+
position: Types.Position.make(position.row, position.col),
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
| None => None
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Types from "../../Core/Types.mjs";
|
|
4
|
+
import * as AlignmentCalc from "../AlignmentCalc.mjs";
|
|
5
|
+
import * as ElementParser from "./ElementParser.mjs";
|
|
6
|
+
|
|
7
|
+
let quotedPattern = /^\s*'([^']+)'\s*$/;
|
|
8
|
+
|
|
9
|
+
let quickTestPattern = /'.+'/;
|
|
10
|
+
|
|
11
|
+
function canParse(content) {
|
|
12
|
+
return quickTestPattern.test(content);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parse(content, position, bounds) {
|
|
16
|
+
let result = quotedPattern.exec(content);
|
|
17
|
+
if (result == null) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let matches = result.slice(1);
|
|
21
|
+
let quotedText = matches[0];
|
|
22
|
+
if (quotedText === undefined) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let trimmedText = quotedText.trim();
|
|
26
|
+
if (trimmedText === "") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let align = AlignmentCalc.calculateWithStrategy(content, position, bounds, "RespectPosition");
|
|
30
|
+
return {
|
|
31
|
+
TAG: "Text",
|
|
32
|
+
content: trimmedText,
|
|
33
|
+
emphasis: true,
|
|
34
|
+
position: Types.Position.make(position.row, position.col),
|
|
35
|
+
align: align
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function make() {
|
|
40
|
+
return ElementParser.make(75, canParse, parse);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
quotedPattern,
|
|
45
|
+
quickTestPattern,
|
|
46
|
+
canParse,
|
|
47
|
+
parse,
|
|
48
|
+
make,
|
|
49
|
+
}
|
|
50
|
+
/* Types Not a pure module */
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// CodeTextParser.res
|
|
2
|
+
// Parser for aligned text syntax: 'text'
|
|
3
|
+
//
|
|
4
|
+
// Recognizes single-quote-wrapped text and applies position-based alignment.
|
|
5
|
+
// Unlike regular text (which defaults to left), quoted text respects its
|
|
6
|
+
// position within the parent container for alignment calculation.
|
|
7
|
+
//
|
|
8
|
+
// Examples:
|
|
9
|
+
// - 'Submit' at center position -> Center aligned
|
|
10
|
+
// - 'Cancel' at left position -> Left aligned
|
|
11
|
+
// - 'Next' at right position -> Right aligned
|
|
12
|
+
|
|
13
|
+
open Types
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Quoted text pattern regex: 'text'
|
|
17
|
+
* Matches text wrapped in single quotes
|
|
18
|
+
* Examples:
|
|
19
|
+
* - 'Submit'
|
|
20
|
+
* - 'Centered Text'
|
|
21
|
+
* - ' spaced '
|
|
22
|
+
*/
|
|
23
|
+
let quotedPattern = %re("/^\s*'([^']+)'\s*$/")
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Quick test pattern for canParse (optimized for speed).
|
|
27
|
+
* Just checks if content contains single quotes wrapping text
|
|
28
|
+
*/
|
|
29
|
+
let quickTestPattern = %re("/'.+'/")
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if content matches quoted text syntax pattern.
|
|
33
|
+
*
|
|
34
|
+
* @param content - The text content to test
|
|
35
|
+
* @return true if content looks like quoted text
|
|
36
|
+
*/
|
|
37
|
+
let canParse = (content: string): bool => {
|
|
38
|
+
Js.Re.test_(quickTestPattern, content)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse quoted text element from content.
|
|
43
|
+
*
|
|
44
|
+
* Extracts text from 'text' syntax and creates a Text element with:
|
|
45
|
+
* - Position-based alignment (RespectPosition strategy)
|
|
46
|
+
* - emphasis: true (indicates special formatting)
|
|
47
|
+
*
|
|
48
|
+
* This allows single-quote-wrapped text to be centered or right-aligned
|
|
49
|
+
* based on its position within the parent container, unlike regular
|
|
50
|
+
* text which always defaults to left alignment.
|
|
51
|
+
*
|
|
52
|
+
* @param content - The text content containing quote syntax
|
|
53
|
+
* @param position - Grid position where quoted text appears
|
|
54
|
+
* @param bounds - Bounding box of containing box (for alignment calculation)
|
|
55
|
+
* @return Some(Text element) or None
|
|
56
|
+
*/
|
|
57
|
+
let parse = (
|
|
58
|
+
content: string,
|
|
59
|
+
position: Position.t,
|
|
60
|
+
bounds: Bounds.t,
|
|
61
|
+
): option<Types.element> => {
|
|
62
|
+
switch quotedPattern->RegExp.exec(content) {
|
|
63
|
+
| None => None
|
|
64
|
+
| Some(result) => {
|
|
65
|
+
let matches = result->RegExp.Result.matches
|
|
66
|
+
// RegExp.Result.matches slices off the full match, so matches[0] is the first captured group
|
|
67
|
+
switch matches[0] {
|
|
68
|
+
| None => None
|
|
69
|
+
| Some(quotedText) => {
|
|
70
|
+
let trimmedText = quotedText->String.trim
|
|
71
|
+
|
|
72
|
+
// Return None for empty quoted text
|
|
73
|
+
if trimmedText === "" {
|
|
74
|
+
None
|
|
75
|
+
} else {
|
|
76
|
+
// Calculate alignment based on position within bounds
|
|
77
|
+
// Quoted text uses RespectPosition strategy for proper alignment
|
|
78
|
+
let align = AlignmentCalc.calculateWithStrategy(
|
|
79
|
+
content,
|
|
80
|
+
position,
|
|
81
|
+
bounds,
|
|
82
|
+
AlignmentCalc.RespectPosition,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// Create Text element with emphasis=true to indicate special formatting
|
|
86
|
+
Some(
|
|
87
|
+
Types.Text({
|
|
88
|
+
content: trimmedText,
|
|
89
|
+
emphasis: true,
|
|
90
|
+
position: Types.Position.make(position.row, position.col),
|
|
91
|
+
align: align,
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a CodeTextParser instance.
|
|
103
|
+
*
|
|
104
|
+
* Priority: 75 (between EmphasisParser and LinkParser)
|
|
105
|
+
* Higher than emphasis but lower than links/buttons
|
|
106
|
+
*
|
|
107
|
+
* @return An ElementParser configured for quoted text recognition
|
|
108
|
+
*/
|
|
109
|
+
let make = (): ElementParser.elementParser => {
|
|
110
|
+
ElementParser.make(~priority=75, ~canParse, ~parse)
|
|
111
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// ElementParser.res
|
|
2
|
+
// Interface for all element parsers in the semantic parsing stage
|
|
3
|
+
//
|
|
4
|
+
// This module defines the contract that all element parsers must implement
|
|
5
|
+
// to be registered in the ParserRegistry and participate in element recognition.
|
|
6
|
+
|
|
7
|
+
open Types
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Result type for parsing operations.
|
|
11
|
+
* Returns Some(element) if parsing succeeds, None if the pattern doesn't match.
|
|
12
|
+
*/
|
|
13
|
+
type parseResult<'element> = option<'element>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ElementParser interface type.
|
|
17
|
+
*
|
|
18
|
+
* All element parsers (ButtonParser, InputParser, LinkParser, etc.) must
|
|
19
|
+
* implement this interface to be compatible with the ParserRegistry.
|
|
20
|
+
*
|
|
21
|
+
* Fields:
|
|
22
|
+
* - priority: Determines the order in which parsers are tried (higher = earlier)
|
|
23
|
+
* Recommended ranges: 100 (buttons), 90 (inputs), 80 (links), 70 (emphasis), 1 (text fallback)
|
|
24
|
+
* - canParse: Quick check function to determine if this parser can handle the content
|
|
25
|
+
* - parse: Full parsing function that extracts element data from content
|
|
26
|
+
*/
|
|
27
|
+
type t<'position, 'bounds, 'element> = {
|
|
28
|
+
/**
|
|
29
|
+
* Priority value for parser ordering.
|
|
30
|
+
* Higher priority parsers are checked first.
|
|
31
|
+
* Use 1 for fallback parsers, 100+ for specific element types.
|
|
32
|
+
*/
|
|
33
|
+
priority: int,
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Quick pattern matching function.
|
|
37
|
+
* Returns true if this parser can handle the given content string.
|
|
38
|
+
* Should be fast and use simple pattern matching (regex test, string contains, etc.)
|
|
39
|
+
*
|
|
40
|
+
* @param content - The text content to check
|
|
41
|
+
* @return true if this parser should attempt to parse the content
|
|
42
|
+
*/
|
|
43
|
+
canParse: string => bool,
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Full parsing function.
|
|
47
|
+
* Extracts element data from content and returns a structured element.
|
|
48
|
+
* Only called if canParse() returns true.
|
|
49
|
+
*
|
|
50
|
+
* @param content - The text content to parse
|
|
51
|
+
* @param position - Position in the grid where this content was found
|
|
52
|
+
* @param bounds - Bounding box of the containing box (for alignment calculation)
|
|
53
|
+
* @return Some(element) if parsing succeeds, None if it fails
|
|
54
|
+
*/
|
|
55
|
+
parse: (string, 'position, 'bounds) => parseResult<'element>,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Concrete type alias for element parsers using the actual types.
|
|
60
|
+
* This will be used by the ParserRegistry and concrete parser implementations.
|
|
61
|
+
*
|
|
62
|
+
* Note: This assumes Position.t, Bounds.t, and element types are defined in Core/Types.res
|
|
63
|
+
* The generic version above allows for testing without those dependencies.
|
|
64
|
+
*/
|
|
65
|
+
type elementParser = t<Position.t, Bounds.t, Types.element>
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Helper function to create an element parser.
|
|
69
|
+
*
|
|
70
|
+
* @param priority - Priority value for ordering
|
|
71
|
+
* @param canParse - Pattern matching function
|
|
72
|
+
* @param parse - Full parsing function
|
|
73
|
+
* @return An ElementParser.t instance
|
|
74
|
+
*/
|
|
75
|
+
let make = (
|
|
76
|
+
~priority: int,
|
|
77
|
+
~canParse: string => bool,
|
|
78
|
+
~parse: (string, Position.t, Bounds.t) => parseResult<Types.element>,
|
|
79
|
+
): elementParser => {
|
|
80
|
+
priority,
|
|
81
|
+
canParse,
|
|
82
|
+
parse,
|
|
83
|
+
}
|