@slxu/graphsx 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/src/markup.js ADDED
@@ -0,0 +1,219 @@
1
+ import { GraphDslError } from "./errors.js";
2
+ import { parseBraceLiteral } from "./literals.js";
3
+
4
+ export function parseMarkup(source) {
5
+ const parser = new MarkupParser(source);
6
+ return parser.parseDocument();
7
+ }
8
+
9
+ class MarkupParser {
10
+ constructor(source) {
11
+ this.source = source;
12
+ this.index = 0;
13
+ }
14
+
15
+ parseDocument() {
16
+ const nodes = [];
17
+ while (!this.isDone()) {
18
+ this.skipWhitespace();
19
+ if (this.isDone()) break;
20
+ nodes.push(this.parseElement());
21
+ }
22
+ return nodes;
23
+ }
24
+
25
+ parseElement() {
26
+ this.expect("<");
27
+ if (this.peek() === "/") {
28
+ throw new GraphDslError("Unexpected closing tag", this.index);
29
+ }
30
+
31
+ const name = this.readName();
32
+ const attrs = this.readAttrs();
33
+
34
+ if (this.consume("/>")) {
35
+ return { type: "element", name, attrs, children: [] };
36
+ }
37
+
38
+ this.expect(">");
39
+ const children = [];
40
+
41
+ while (!this.isDone()) {
42
+ this.skipWhitespace();
43
+ if (this.consume(`</${name}>`)) {
44
+ return { type: "element", name, attrs, children };
45
+ }
46
+ if (this.peek() === "<") {
47
+ children.push(this.parseElement());
48
+ } else {
49
+ const text = this.readText();
50
+ if (text.trim()) {
51
+ children.push({ type: "text", value: text });
52
+ }
53
+ }
54
+ }
55
+
56
+ throw new GraphDslError(`Missing closing tag for <${name}>`, this.index);
57
+ }
58
+
59
+ readAttrs() {
60
+ const attrs = {};
61
+
62
+ while (!this.isDone()) {
63
+ this.skipWhitespace({ comments: false });
64
+ const char = this.peek();
65
+ if (char === ">" || (char === "/" && this.source[this.index + 1] === ">")) {
66
+ return attrs;
67
+ }
68
+
69
+ const name = this.readName();
70
+ this.skipWhitespace({ comments: false });
71
+
72
+ if (!this.consume("=")) {
73
+ attrs[name] = true;
74
+ continue;
75
+ }
76
+
77
+ this.skipWhitespace({ comments: false });
78
+ attrs[name] = this.readAttrValue();
79
+ }
80
+
81
+ return attrs;
82
+ }
83
+
84
+ readAttrValue() {
85
+ const quote = this.peek();
86
+ if (quote === '"' || quote === "'") {
87
+ this.index += 1;
88
+ const start = this.index;
89
+ while (!this.isDone() && this.peek() !== quote) this.index += 1;
90
+ const value = this.source.slice(start, this.index);
91
+ this.expect(quote);
92
+ return value;
93
+ }
94
+
95
+ if (quote === "{") {
96
+ return parseBraceLiteral(this.readBraced());
97
+ }
98
+
99
+ throw new GraphDslError("Attribute values must be quoted or braced", this.index);
100
+ }
101
+
102
+ readBraced() {
103
+ this.expect("{");
104
+ const start = this.index;
105
+ let depth = 1;
106
+
107
+ while (!this.isDone()) {
108
+ const char = this.peek();
109
+ if (char === "{") depth += 1;
110
+ if (char === "}") depth -= 1;
111
+ if (depth === 0) {
112
+ const value = this.source.slice(start, this.index);
113
+ this.index += 1;
114
+ return value;
115
+ }
116
+ this.index += 1;
117
+ }
118
+
119
+ throw new GraphDslError("Unclosed braced value", start);
120
+ }
121
+
122
+ readName() {
123
+ const start = this.index;
124
+ while (!this.isDone() && /[A-Za-z0-9_.:-]/.test(this.peek())) {
125
+ this.index += 1;
126
+ }
127
+
128
+ if (start === this.index) {
129
+ throw new GraphDslError("Expected name", this.index);
130
+ }
131
+
132
+ return this.source.slice(start, this.index);
133
+ }
134
+
135
+ readText() {
136
+ const start = this.index;
137
+ while (!this.isDone() && this.peek() !== "<") {
138
+ this.index += 1;
139
+ }
140
+ return this.source.slice(start, this.index);
141
+ }
142
+
143
+ skipWhitespace(options = {}) {
144
+ const comments = options.comments !== false;
145
+
146
+ while (!this.isDone()) {
147
+ if (/\s/.test(this.peek())) {
148
+ this.index += 1;
149
+ continue;
150
+ }
151
+ if (comments && this.skipComment()) {
152
+ continue;
153
+ }
154
+ break;
155
+ }
156
+ }
157
+
158
+ skipComment() {
159
+ if (this.consume("{/*")) {
160
+ const end = this.source.indexOf("*/}", this.index);
161
+ if (end === -1) {
162
+ throw new GraphDslError("Unclosed JSX comment", this.index);
163
+ }
164
+ this.index = end + 3;
165
+ return true;
166
+ }
167
+
168
+ if (this.consume("<!--")) {
169
+ const end = this.source.indexOf("-->", this.index);
170
+ if (end === -1) {
171
+ throw new GraphDslError("Unclosed HTML comment", this.index);
172
+ }
173
+ this.index = end + 3;
174
+ return true;
175
+ }
176
+
177
+ if (this.consume("{")) {
178
+ this.skipBracedComment();
179
+ return true;
180
+ }
181
+
182
+ return false;
183
+ }
184
+
185
+ skipBracedComment() {
186
+ const start = this.index;
187
+ let depth = 1;
188
+
189
+ while (!this.isDone()) {
190
+ const char = this.peek();
191
+ if (char === "{") depth += 1;
192
+ if (char === "}") depth -= 1;
193
+ this.index += 1;
194
+ if (depth === 0) return;
195
+ }
196
+
197
+ throw new GraphDslError("Unclosed braced comment", start);
198
+ }
199
+
200
+ consume(value) {
201
+ if (!this.source.startsWith(value, this.index)) return false;
202
+ this.index += value.length;
203
+ return true;
204
+ }
205
+
206
+ expect(value) {
207
+ if (!this.consume(value)) {
208
+ throw new GraphDslError(`Expected "${value}"`, this.index);
209
+ }
210
+ }
211
+
212
+ peek() {
213
+ return this.source[this.index];
214
+ }
215
+
216
+ isDone() {
217
+ return this.index >= this.source.length;
218
+ }
219
+ }