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,46 @@
|
|
|
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 emphasisPattern = /^\s*\*\s+(.+)$/;
|
|
8
|
+
|
|
9
|
+
let quickTestPattern = /^\s*\*\s/;
|
|
10
|
+
|
|
11
|
+
function canParse(content) {
|
|
12
|
+
return quickTestPattern.test(content);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parse(content, position, bounds) {
|
|
16
|
+
let result = emphasisPattern.exec(content);
|
|
17
|
+
if (result == null) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let matches = result.slice(1);
|
|
21
|
+
let textContent = matches[0];
|
|
22
|
+
if (textContent === undefined) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let align = AlignmentCalc.calculateWithStrategy(content, position, bounds, "RespectPosition");
|
|
26
|
+
return {
|
|
27
|
+
TAG: "Text",
|
|
28
|
+
content: textContent,
|
|
29
|
+
emphasis: true,
|
|
30
|
+
position: Types.Position.make(position.row, position.col),
|
|
31
|
+
align: align
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function make() {
|
|
36
|
+
return ElementParser.make(70, canParse, parse);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
emphasisPattern,
|
|
41
|
+
quickTestPattern,
|
|
42
|
+
canParse,
|
|
43
|
+
parse,
|
|
44
|
+
make,
|
|
45
|
+
}
|
|
46
|
+
/* Types Not a pure module */
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// EmphasisParser.res
|
|
2
|
+
// Parser for emphasis text syntax: * Text
|
|
3
|
+
|
|
4
|
+
open Types
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Emphasis pattern regex: ^\s*\*\s+(.+)
|
|
8
|
+
* Matches: * Important text
|
|
9
|
+
*/
|
|
10
|
+
let emphasisPattern = %re("/^\s*\*\s+(.+)$/")
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Quick test pattern for canParse (optimized for speed).
|
|
14
|
+
*/
|
|
15
|
+
let quickTestPattern = %re("/^\s*\*\s/")
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if content matches emphasis syntax pattern.
|
|
19
|
+
*/
|
|
20
|
+
let canParse = (content: string): bool => {
|
|
21
|
+
Js.Re.test_(quickTestPattern, content)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse emphasis element from content.
|
|
26
|
+
*/
|
|
27
|
+
let parse = (
|
|
28
|
+
content: string,
|
|
29
|
+
position: Position.t,
|
|
30
|
+
bounds: Bounds.t,
|
|
31
|
+
): option<Types.element> => {
|
|
32
|
+
switch emphasisPattern->RegExp.exec(content) {
|
|
33
|
+
| None => None
|
|
34
|
+
| Some(result) => {
|
|
35
|
+
// RegExp.Result.matches slices off the full match, so matches[0] is the first captured group
|
|
36
|
+
let matches = result->RegExp.Result.matches
|
|
37
|
+
switch matches[0] {
|
|
38
|
+
| None => None
|
|
39
|
+
| Some(textContent) => {
|
|
40
|
+
let align = AlignmentCalc.calculateWithStrategy(
|
|
41
|
+
content,
|
|
42
|
+
position,
|
|
43
|
+
bounds,
|
|
44
|
+
AlignmentCalc.RespectPosition,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
Some(
|
|
48
|
+
Types.Text({
|
|
49
|
+
content: textContent,
|
|
50
|
+
emphasis: true,
|
|
51
|
+
position: Types.Position.make(position.row, position.col),
|
|
52
|
+
align: align,
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create an EmphasisParser instance.
|
|
63
|
+
* Priority: 70
|
|
64
|
+
*/
|
|
65
|
+
let make = (): ElementParser.elementParser => {
|
|
66
|
+
ElementParser.make(~priority=70, ~canParse, ~parse)
|
|
67
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 inputPattern = /(?:^|[^#])#(\w+)\s*$/;
|
|
7
|
+
|
|
8
|
+
function canParse(content) {
|
|
9
|
+
let trimmed = content.trim();
|
|
10
|
+
return inputPattern.test(trimmed);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parse(content, position, _bounds) {
|
|
14
|
+
let trimmed = content.trim();
|
|
15
|
+
let result = inputPattern.exec(trimmed);
|
|
16
|
+
if (result == null) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let matches = result.slice(1);
|
|
20
|
+
let identifier = matches[0];
|
|
21
|
+
if (identifier !== undefined) {
|
|
22
|
+
return {
|
|
23
|
+
TAG: "Input",
|
|
24
|
+
id: identifier,
|
|
25
|
+
placeholder: undefined,
|
|
26
|
+
position: Types.Position.make(position.row, position.col)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function make() {
|
|
32
|
+
return ElementParser.make(90, canParse, parse);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
inputPattern,
|
|
37
|
+
canParse,
|
|
38
|
+
parse,
|
|
39
|
+
make,
|
|
40
|
+
}
|
|
41
|
+
/* Types Not a pure module */
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// InputParser.res
|
|
2
|
+
|
|
3
|
+
open Types
|
|
4
|
+
// Parser for input field elements with syntax "#fieldname"
|
|
5
|
+
//
|
|
6
|
+
// Recognizes input fields denoted by a hash (#) followed by an identifier.
|
|
7
|
+
// Example: "#email", "#password", "#username"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regular expression pattern for input field syntax.
|
|
11
|
+
* Matches: # followed by one or more word characters (letters, digits, underscores)
|
|
12
|
+
* The field must be at the end of the string (with optional trailing whitespace).
|
|
13
|
+
* Can appear anywhere in the line, optionally preceded by a label.
|
|
14
|
+
* Examples:
|
|
15
|
+
* - "#email" ✓
|
|
16
|
+
* - "#password123" ✓
|
|
17
|
+
* - "#user_name" ✓
|
|
18
|
+
* - "Email: #email" ✓ (with label prefix)
|
|
19
|
+
* - "Password: #password" ✓ (with label prefix)
|
|
20
|
+
* - "#first-name" ✗ (contains hyphen after word characters)
|
|
21
|
+
* - "#email.address" ✗ (dot not allowed)
|
|
22
|
+
* - "##email" ✗ (double hash not allowed)
|
|
23
|
+
* - "# email" ✗ (space after #)
|
|
24
|
+
*/
|
|
25
|
+
// Pattern: (start or non-#) + # + word chars + optional trailing whitespace + end
|
|
26
|
+
let inputPattern = %re("/(?:^|[^#])#(\w+)\s*$/")
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Quick check if content matches input field pattern.
|
|
30
|
+
*
|
|
31
|
+
* @param content - Text content to check
|
|
32
|
+
* @return true if content contains # followed by word characters
|
|
33
|
+
*/
|
|
34
|
+
let canParse = (content: string): bool => {
|
|
35
|
+
let trimmed = content->String.trim
|
|
36
|
+
|
|
37
|
+
// Check if contains #\w+ pattern anywhere in the string
|
|
38
|
+
inputPattern->RegExp.test(trimmed)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse input field element from content.
|
|
43
|
+
*
|
|
44
|
+
* Extracts the field identifier after the # character and creates
|
|
45
|
+
* an Input element with that ID.
|
|
46
|
+
*
|
|
47
|
+
* @param content - Text content to parse (e.g., "#email")
|
|
48
|
+
* @param position - Grid position where input was found
|
|
49
|
+
* @param _bounds - Bounding box (not used for inputs, they are always left-aligned)
|
|
50
|
+
* @return Some(Input element) if parsing succeeds, None otherwise
|
|
51
|
+
*/
|
|
52
|
+
let parse = (
|
|
53
|
+
content: string,
|
|
54
|
+
position: Position.t,
|
|
55
|
+
_bounds: Bounds.t,
|
|
56
|
+
): ElementParser.parseResult<Types.element> => {
|
|
57
|
+
let trimmed = content->String.trim
|
|
58
|
+
|
|
59
|
+
// Extract identifier after # character
|
|
60
|
+
switch inputPattern->RegExp.exec(trimmed) {
|
|
61
|
+
| Some(result) => {
|
|
62
|
+
// Get first capture group (the identifier after #)
|
|
63
|
+
// RegExp.Result.matches slices off the full match, so matches[0] is the first captured group
|
|
64
|
+
let matches = result->RegExp.Result.matches
|
|
65
|
+
switch matches[0] {
|
|
66
|
+
| Some(identifier) => {
|
|
67
|
+
// Successfully extracted identifier
|
|
68
|
+
Some(
|
|
69
|
+
Types.Input({
|
|
70
|
+
id: identifier,
|
|
71
|
+
placeholder: None,
|
|
72
|
+
position: Types.Position.make(position.row, position.col),
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
| None => None
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
| None => None
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create an InputParser instance with priority 90.
|
|
85
|
+
*
|
|
86
|
+
* Priority explanation:
|
|
87
|
+
* - 100: Buttons (highest priority)
|
|
88
|
+
* - 90: Inputs (this parser)
|
|
89
|
+
* - 80: Links
|
|
90
|
+
* - 70: Emphasis
|
|
91
|
+
* - 1: Text (fallback, lowest priority)
|
|
92
|
+
*
|
|
93
|
+
* @return ElementParser.elementParser configured for input fields
|
|
94
|
+
*/
|
|
95
|
+
let make = (): ElementParser.elementParser => {
|
|
96
|
+
ElementParser.make(~priority=90, ~canParse, ~parse)
|
|
97
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 linkPattern = /"((?:[^"\\]|\\.)+)"/;
|
|
7
|
+
|
|
8
|
+
let quickPattern = /"(?:[^"\\]|\\.)+"/;
|
|
9
|
+
|
|
10
|
+
function slugify(text) {
|
|
11
|
+
return text.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function unescapeQuotes(text) {
|
|
15
|
+
return (text.replace(/\\"/g, '"'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function canParse(content) {
|
|
19
|
+
return quickPattern.test(content);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parse(content, position, _bounds) {
|
|
23
|
+
let result = linkPattern.exec(content);
|
|
24
|
+
if (result == null) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let matches = result.slice(1);
|
|
28
|
+
let linkText = matches[0];
|
|
29
|
+
if (linkText === undefined) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (linkText.trim() === "") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let unescapedText = unescapeQuotes(linkText);
|
|
36
|
+
let linkId = slugify(unescapedText);
|
|
37
|
+
return {
|
|
38
|
+
TAG: "Link",
|
|
39
|
+
id: linkId,
|
|
40
|
+
text: unescapedText,
|
|
41
|
+
position: Types.Position.make(position.row, position.col),
|
|
42
|
+
align: "Left",
|
|
43
|
+
actions: []
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function make() {
|
|
48
|
+
return ElementParser.make(80, canParse, parse);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
linkPattern,
|
|
53
|
+
quickPattern,
|
|
54
|
+
slugify,
|
|
55
|
+
unescapeQuotes,
|
|
56
|
+
canParse,
|
|
57
|
+
parse,
|
|
58
|
+
make,
|
|
59
|
+
}
|
|
60
|
+
/* Types Not a pure module */
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// LinkParser.res
|
|
2
|
+
|
|
3
|
+
open Types
|
|
4
|
+
// Parser for link syntax: "Link Text" (quoted text)
|
|
5
|
+
//
|
|
6
|
+
// Recognizes quoted text patterns and generates Link elements.
|
|
7
|
+
// Priority: 80 (between inputs and checkboxes)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regular expression pattern for matching quoted text.
|
|
11
|
+
* Matches: "text content" where content can include escaped quotes (\")
|
|
12
|
+
*
|
|
13
|
+
* Pattern breakdown:
|
|
14
|
+
* - " : Opening quote
|
|
15
|
+
* - ((?:[^"\\]|\\.)+) : One or more of either:
|
|
16
|
+
* - [^"\\] : any character except quote or backslash
|
|
17
|
+
* - \\. : a backslash followed by any character (handles escaped quotes)
|
|
18
|
+
* - " : Closing quote
|
|
19
|
+
*
|
|
20
|
+
* Using %raw to avoid ReScript's regex escaping complexity
|
|
21
|
+
*/
|
|
22
|
+
let linkPattern: Js.Re.t = %raw(`/"((?:[^"\\]|\\.)+)"/`)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Quick check pattern for canParse (faster, no capture groups)
|
|
26
|
+
* Handles escaped quotes within the quoted text
|
|
27
|
+
*/
|
|
28
|
+
let quickPattern: Js.Re.t = %raw(`/"(?:[^"\\]|\\.)+"/`)
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert text to a URL-friendly slug identifier.
|
|
32
|
+
*
|
|
33
|
+
* Rules:
|
|
34
|
+
* - Convert to lowercase
|
|
35
|
+
* - Replace spaces and special characters with hyphens
|
|
36
|
+
* - Remove consecutive hyphens
|
|
37
|
+
* - Trim leading/trailing hyphens
|
|
38
|
+
*
|
|
39
|
+
* Examples:
|
|
40
|
+
* - "Login Here" -> "login-here"
|
|
41
|
+
* - "Sign Up!" -> "sign-up"
|
|
42
|
+
* - " Multiple Spaces " -> "multiple-spaces"
|
|
43
|
+
*
|
|
44
|
+
* @param text - The text to slugify
|
|
45
|
+
* @return URL-friendly identifier
|
|
46
|
+
*/
|
|
47
|
+
let slugify = (text: string): string => {
|
|
48
|
+
text
|
|
49
|
+
->String.toLowerCase
|
|
50
|
+
->String.trim
|
|
51
|
+
// Replace whitespace and special chars with hyphens
|
|
52
|
+
->Js.String2.replaceByRe(%re("/[^a-z0-9]+/g"), "-")
|
|
53
|
+
// Remove consecutive hyphens
|
|
54
|
+
->Js.String2.replaceByRe(%re("/-+/g"), "-")
|
|
55
|
+
// Trim hyphens from start and end
|
|
56
|
+
->Js.String2.replaceByRe(%re("/^-+|-+$/g"), "")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Unescape quotes within the link text.
|
|
61
|
+
* Handles escaped quotes (\") within the text content.
|
|
62
|
+
*
|
|
63
|
+
* @param text - The extracted text with potential escape sequences
|
|
64
|
+
* @return Text with escape sequences resolved
|
|
65
|
+
*/
|
|
66
|
+
let unescapeQuotes = (text: string): string => {
|
|
67
|
+
// Match backslash followed by quote and replace with just quote
|
|
68
|
+
// Use raw JS to avoid ReScript escaping complexity
|
|
69
|
+
%raw(`text.replace(/\\"/g, '"')`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if content matches the link pattern.
|
|
74
|
+
*
|
|
75
|
+
* This is a fast pattern check that doesn't extract data.
|
|
76
|
+
* Used by the parser registry to quickly determine if this parser should be tried.
|
|
77
|
+
*
|
|
78
|
+
* @param content - The content string to check
|
|
79
|
+
* @return true if content matches link syntax
|
|
80
|
+
*/
|
|
81
|
+
let canParse = (content: string): bool => {
|
|
82
|
+
quickPattern->Js.Re.test_(content)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse link content and generate a Link element.
|
|
87
|
+
*
|
|
88
|
+
* Extracts text between quotes and creates a Link element with:
|
|
89
|
+
* - id: Slugified version of the link text
|
|
90
|
+
* - text: The extracted link text
|
|
91
|
+
* - position: Grid position of the link
|
|
92
|
+
* - align: Left alignment (default for links)
|
|
93
|
+
*
|
|
94
|
+
* @param content - The content string to parse
|
|
95
|
+
* @param position - Position in the grid
|
|
96
|
+
* @param _bounds - Bounding box (unused for now, will be used for alignment calculation)
|
|
97
|
+
* @return Some(Link element) if parsing succeeds, None otherwise
|
|
98
|
+
*/
|
|
99
|
+
let parse = (
|
|
100
|
+
content: string,
|
|
101
|
+
position: Position.t,
|
|
102
|
+
_bounds: Bounds.t,
|
|
103
|
+
): ElementParser.parseResult<Types.element> => {
|
|
104
|
+
switch linkPattern->RegExp.exec(content) {
|
|
105
|
+
| Some(result) => {
|
|
106
|
+
// RegExp.Result.matches slices off the full match, so matches[0] is the first captured group
|
|
107
|
+
let matches = result->RegExp.Result.matches
|
|
108
|
+
|
|
109
|
+
switch matches[0] {
|
|
110
|
+
| Some(linkText) => {
|
|
111
|
+
// linkText is already a string, no need for Nullable conversion
|
|
112
|
+
|
|
113
|
+
// Check for empty text
|
|
114
|
+
if linkText->String.trim === "" {
|
|
115
|
+
None
|
|
116
|
+
} else {
|
|
117
|
+
// Unescape any escaped quotes in the text
|
|
118
|
+
let unescapedText = unescapeQuotes(linkText)
|
|
119
|
+
|
|
120
|
+
// Generate ID from text
|
|
121
|
+
let linkId = slugify(unescapedText)
|
|
122
|
+
|
|
123
|
+
// Create Link element
|
|
124
|
+
// Note: Using Left alignment as default
|
|
125
|
+
// When AlignmentCalc is implemented, this will use calculated alignment
|
|
126
|
+
Some(
|
|
127
|
+
Types.Link({
|
|
128
|
+
id: linkId,
|
|
129
|
+
text: unescapedText,
|
|
130
|
+
position: Types.Position.make(position.row, position.col),
|
|
131
|
+
align: Left,
|
|
132
|
+
actions: [],
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
| None => None
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
| None => None
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create a LinkParser instance with priority 80.
|
|
146
|
+
*
|
|
147
|
+
* Priority 80 places it:
|
|
148
|
+
* - After buttons (100) and inputs (90)
|
|
149
|
+
* - Before checkboxes (85) and emphasis (70)
|
|
150
|
+
* - Well before text fallback (1)
|
|
151
|
+
*
|
|
152
|
+
* @return ElementParser.t configured for link parsing
|
|
153
|
+
*/
|
|
154
|
+
let make = (): ElementParser.elementParser => {
|
|
155
|
+
ElementParser.make(~priority=80, ~canParse, ~parse)
|
|
156
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
function make() {
|
|
7
|
+
return ElementParser.make(1, _content => true, (content, position, _bounds) => ({
|
|
8
|
+
TAG: "Text",
|
|
9
|
+
content: content,
|
|
10
|
+
emphasis: false,
|
|
11
|
+
position: Types.Position.make(position.row, position.col),
|
|
12
|
+
align: "Left"
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
make,
|
|
18
|
+
}
|
|
19
|
+
/* Types Not a pure module */
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// TextParser.res
|
|
2
|
+
// Fallback parser for plain text that doesn't match any other pattern
|
|
3
|
+
//
|
|
4
|
+
// This parser has the lowest priority (1) and always accepts content,
|
|
5
|
+
// making it the last resort when no other parser matches.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a TextParser instance.
|
|
9
|
+
*
|
|
10
|
+
* This is a fallback parser that catches all unrecognized content and
|
|
11
|
+
* treats it as plain text. It should be registered with the lowest priority
|
|
12
|
+
* in the ParserRegistry.
|
|
13
|
+
*
|
|
14
|
+
* Behavior:
|
|
15
|
+
* - canParse: Always returns true (catches everything)
|
|
16
|
+
* - parse: Generates a Text element with emphasis=false
|
|
17
|
+
* - Priority: 1 (lowest - used as fallback)
|
|
18
|
+
*
|
|
19
|
+
* @return An ElementParser instance configured as a text fallback parser
|
|
20
|
+
*/
|
|
21
|
+
let make = (): ElementParser.elementParser => {
|
|
22
|
+
ElementParser.make(
|
|
23
|
+
~priority=1, // Lowest priority - fallback parser
|
|
24
|
+
~canParse=_content => {
|
|
25
|
+
// Always returns true to catch any content that no other parser matched
|
|
26
|
+
true
|
|
27
|
+
},
|
|
28
|
+
~parse=(content, position, _bounds) => {
|
|
29
|
+
// Generate a plain text element with the raw content
|
|
30
|
+
// Note: For fallback text, we use Left alignment by default
|
|
31
|
+
// This is simpler than calculating alignment for unstructured text
|
|
32
|
+
Some(
|
|
33
|
+
Types.Text({
|
|
34
|
+
content: content,
|
|
35
|
+
emphasis: false,
|
|
36
|
+
position: Types.Position.make(position.row, position.col),
|
|
37
|
+
align: Types.Left,
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
}
|