@marrs/sxml 0.3.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/parser.js ADDED
@@ -0,0 +1,1012 @@
1
+ import { Readable } from 'stream';
2
+ import { Buffer_Trait, Sexp_Buffer_Trait } from './buffer.js'
3
+ import { last, last_but, Interface } from './util.js';
4
+ import { pre_html_chars, pre_hex_chars, pre_html_entity, pre_hex_entity, pre_qualified_ns, unqualified_xml } from './processors/index.js';
5
+
6
+ // String catenation by joining an array is a bit faster
7
+ // than using +. Therefore we prefer to build an array
8
+ // of string parts rather than appending to a preexisting
9
+ // string variable.
10
+
11
+ function is_found(idxOrStr, substr) {
12
+ if (void 0 === substr) {
13
+ return idxOrStr > -1;
14
+ }
15
+ return idxOrStr.indexOf(substr) > -1;
16
+ }
17
+
18
+ function identify_operator(token) {
19
+ if (token.length) {
20
+ var firstChar = token.charAt(0);
21
+ if (/\s/.test(firstChar)) {
22
+ return 'none';
23
+ }
24
+ switch(firstChar) {
25
+ case "@": return 'attr';
26
+ case ")": return 'none';
27
+ case "(": return 'bracket';
28
+ case "&": return 'specialchar';
29
+ case "<": return 'specialchar';
30
+ case ">": return 'specialchar';
31
+ default: return 'tag';
32
+ }
33
+ }
34
+ return 'none';
35
+ }
36
+
37
+ const iStackFrame = Interface.create({
38
+ assertions: {
39
+ processor: Interface.isDefined,
40
+ data: Interface.isArray
41
+ },
42
+ defaults: {
43
+ processor: null,
44
+ operator: '',
45
+ ast: [''],
46
+ namespace: null
47
+ },
48
+ });
49
+
50
+
51
+ function is_valid_attr_name(name) {
52
+ if (0 === name.length) return false;
53
+ if (is_found(name, '"')) return false;
54
+ if (is_found(name, "'")) return false;
55
+ if (is_found(name, ">")) return false;
56
+ if (is_found(name, "/")) return false;
57
+ if (is_found(name, "=")) return false;
58
+ // Reject control chars
59
+ if (/[\u0000-\u001F\u0020]/.test(name)) return false;
60
+ // Reject nonchars
61
+ if (/[\uFDD0-\uFDEF\uFFFE\uFFFF\u1FFFE\u1FFFF\u2FFFE\u2FFFF\u3FFFE\u3FFFF\u4FFFE\u4FFFF\u5FFFE\u5FFFF\u6FFFE\u6FFFF\u7FFFE\u7FFFF\u8FFFE\u8FFFF\u9FFFE\u9FFFF\uAFFFE\uAFFFF\uBFFFE\uBFFFF\uCFFFE\uCFFFF\uDFFFE\uDFFFF\uEFFFE\uEFFFF\uFFFFE\uFFFFF\u10FFFE\u10FFFF]/.test(name)) return false;
62
+ return true;
63
+ }
64
+
65
+ function is_char_quote_mark(ch) {
66
+ return ['"', "'"].indexOf(ch) > -1;
67
+ }
68
+
69
+ export function log_parse_error(data) {
70
+ console.info("TODO: LOG TO STDERR -", data.msg, "- line:", data.lineNumber, "token:", data.token);
71
+ }
72
+
73
+ function is_within_quote(isQuoting, idxQuote, idxString) {
74
+ if (idxQuote === idxString) {
75
+ throw new Error("Substring and quote char cannot begin at same index");
76
+ }
77
+ if (isQuoting) {
78
+ if (is_found(idxQuote)) {
79
+ return idxQuote > idxString;
80
+ }
81
+ return true;
82
+ }
83
+
84
+ if (is_found(idxQuote)) {
85
+ return idxQuote < idxString;
86
+ }
87
+
88
+ return false;
89
+ }
90
+
91
+ function is_quoting_by(idx, idxQuote, isAlreadyQuoting) {
92
+ if (is_found(idxQuote) && idxQuote <= idx) {
93
+ return !isAlreadyQuoting;
94
+ }
95
+ return isAlreadyQuoting;
96
+ }
97
+
98
+ function index_of_closing_quote(str, quoteChar) {
99
+ var maybeClosingQuote = str.indexOf(quoteChar);
100
+ if (maybeClosingQuote < 0) return -1;
101
+ if (0 === maybeClosingQuote) return 0;
102
+ if ('\\' === str.charAt(maybeClosingQuote -1)) {
103
+ // XXX Could stack-overflow in theory
104
+ return maybeClosingQuote + 1 + index_of_closing_quote(str.substring(maybeClosingQuote +1), quoteChar);
105
+ }
106
+ return maybeClosingQuote;
107
+ }
108
+
109
+ function extend_default_processors(ext) {
110
+ const target = {};
111
+ Object.entries(defaultProcessors).forEach(([ns, proc]) => {
112
+ target[ns] = {
113
+ pre: proc.pre.slice(),
114
+ proc: proc.proc,
115
+ }
116
+ });
117
+ Object.entries(ext).forEach(([ns, proc]) => {
118
+ const extPre = proc?.pre;
119
+ const extProc = proc?.proc;
120
+ if (target[ns]) {
121
+ if(extPre && extPre.length) {
122
+ target[ns].pre = extPre;
123
+ }
124
+ if(extProc) {
125
+ if (typeof extProc !== 'function') {
126
+ throw new Error(`${ns}.proc must be a function`);
127
+ }
128
+ target[ns].proc = extProc;
129
+ }
130
+ } else {
131
+ target[ns] = {
132
+ pre: extPre,
133
+ proc: extProc,
134
+ }
135
+ }
136
+ });
137
+ return target;
138
+ }
139
+
140
+ function make_pipeline(descriptor) {
141
+ if (typeof descriptor === 'function') return descriptor;
142
+ var pres = descriptor.pre || [];
143
+ var proc = descriptor.proc;
144
+ return function(event, stackFrame, content, state, callData) {
145
+ var args = [event, stackFrame, content, state, callData];
146
+ for (var i = 0; i < pres.length; i++) {
147
+ args = pres[i].apply(null, args);
148
+ }
149
+ return proc.apply(null, args);
150
+ };
151
+ }
152
+
153
+ function definitely_comes_before(idx1, idx2) {
154
+ if (idx1 > -1 && idx2 < 0) return true;
155
+ return idx1 > -1 && idx1 < idx2;
156
+ }
157
+
158
+ function track_line_state(content, data) {
159
+ var lastNl = content.lastIndexOf('\n');
160
+ if (lastNl >= 0) {
161
+ data.hadNewline = true;
162
+ data.currentLineIndent = content.substring(lastNl + 1);
163
+ } else {
164
+ data.currentLineIndent += content;
165
+ }
166
+ }
167
+
168
+ const defaultProcessor = 'html';
169
+
170
+ const defaultProcessors = {
171
+ unqualified_xml: {
172
+ pre: [pre_hex_chars, pre_hex_entity],
173
+ proc: unqualified_xml
174
+ },
175
+ html: {
176
+ pre: [pre_html_chars, pre_html_entity],
177
+ proc: unqualified_xml
178
+ },
179
+ xsl: {
180
+ pre: [pre_qualified_ns, pre_hex_chars, pre_hex_entity],
181
+ proc: unqualified_xml
182
+ },
183
+ };
184
+
185
+ export function init_parse_state(initData) {
186
+ initData = initData || {};
187
+ var data = {
188
+ processor: defaultProcessor,
189
+ lineCount: 0,
190
+ offset: 0,
191
+ lexicalStack: {
192
+ frames: [],
193
+ push: function(x) {
194
+ return this.frames.push(x);
195
+ },
196
+ current_frame: function() {
197
+ return last(this.frames);
198
+ },
199
+ parent_frame: function() {
200
+ return last_but(1, this.frames);
201
+ },
202
+ processor: function() {
203
+ if (!this.frames.length) {
204
+ return data.processor || defaultProcessor;
205
+ }
206
+ var currentFrame = this.current_frame();
207
+ var idxColon = (currentFrame.operator || '').indexOf(':');
208
+ switch (idxColon) {
209
+ case -1: return currentFrame.processor || data.processor || defaultProcessor;
210
+ case 0: return currentFrame.processor;
211
+ default: {
212
+ var currentNs = currentFrame.operator.substring(0, idxColon);
213
+ if (data.processors[currentNs]) {
214
+ return currentNs;
215
+ }
216
+ return defaultProcessor;
217
+ }
218
+ }
219
+ },
220
+ pop: function(buf, isTagChildless) {
221
+ var event = isTagChildless? 'write-tag:no-children' : 'close-tag';
222
+ var poppedFrame = this.frames.pop();
223
+ var currentFrame = this.current_frame();
224
+ var poppedFrameAst = poppedFrame.ast;
225
+ switch (identify_operator(poppedFrame.operator)) {
226
+ case 'tag':
227
+ poppedFrame.processedData = data.processors[poppedFrame.processor](
228
+ event,
229
+ poppedFrame,
230
+ '',
231
+ data.processorState[poppedFrame.processor],
232
+ {
233
+ ast: poppedFrameAst,
234
+ cursor: data.cursor,
235
+ source: data.source,
236
+ lineCount: data.lineCount
237
+ }
238
+ );
239
+ break;
240
+ case 'attr':
241
+ if (currentFrame) {
242
+ switch (identify_operator(currentFrame.operator)) {
243
+ case 'tag':
244
+ currentFrame.ast.push(poppedFrameAst);
245
+ break;
246
+ case 'attr':
247
+ currentFrame.ast.push(poppedFrameAst);
248
+ break;
249
+ default:
250
+ console.error("Could not merge attributes. Stack frame did not represent attribute or tag.");
251
+ }
252
+ }
253
+ break;
254
+ default:
255
+ break;
256
+ }
257
+ return poppedFrame;
258
+ }
259
+ },
260
+ state: null,
261
+ wsBuf: '',
262
+ lastChunk: '',
263
+ isPrettyFormatting: initData.isPrettyFormatting || false,
264
+ currentLineIndent: '',
265
+ hadNewline: false,
266
+ readerMacroName: '',
267
+ readerMacroArgs: ''
268
+ };
269
+
270
+ var source = initData.source;
271
+ if (source === undefined || source === null) {
272
+ data.source = process.stdin;
273
+ } else if (typeof source === 'string' || source instanceof Readable) {
274
+ data.source = source;
275
+ } else {
276
+ throw new Error('source must be a string or a Readable stream');
277
+ }
278
+ data.cursor = initData.cursor || 0;
279
+
280
+ data.processors = {};
281
+ data.processorState = {};
282
+
283
+ const processors = extend_default_processors(initData?.extensions || {});
284
+
285
+ Object.entries(processors).forEach(([ns, descriptor]) => {
286
+ data.processors[ns] = make_pipeline(descriptor);
287
+ data.processorState[ns] = iStackFrame.new();
288
+ });
289
+
290
+ data.readers = {
291
+ ext: function(data, args) {
292
+ data.lexicalStack.push(iStackFrame.new({ processor: args, isExtWrapper: true }));
293
+ },
294
+ _default: function(data, args) {
295
+ log_parse_error({ msg: "Unknown reader macro: #" + data.readerMacroName, lineNumber: data.lineCount });
296
+ }
297
+ };
298
+
299
+ if (initData.readers) {
300
+ for (let name in initData.readers) {
301
+ data.readers[name] = initData.readers[name];
302
+ }
303
+ }
304
+
305
+ data.barf_ws = function() {
306
+ var buf = this.wsBuf;
307
+ this.wsBuf = '';
308
+ return buf;
309
+ }
310
+
311
+ return data;
312
+ }
313
+
314
+ export function update_parse_state(strChunk, data, result) {
315
+ parse_chunk(strChunk, data, result);
316
+ data.offset += strChunk.length;
317
+ data.lastChunk = strChunk;
318
+ }
319
+
320
+ // The AST for lexical stack frame data is represented like so:
321
+ // [ LEADING_WHITESPACE, TAG [LEADING_WHITESPACE, ATTR_NAME, ATTR_VALUE], ...]
322
+
323
+ const AST_IDX_WS = 0;
324
+ const AST_IDX_TAG = 1;
325
+ const AST_IDX_NAME = 1;
326
+ const AST_IDX_VAL = 2;
327
+ const AST_IDX_AT1 = 2;
328
+
329
+ export function parse_chunk(strChunk, data, result) {
330
+ var lineLength = strChunk.length;
331
+
332
+ var buf = Object.create(Sexp_Buffer_Trait);
333
+ Object.assign(buf, {
334
+ cursor: 0,
335
+ lineCount: data.lineCount,
336
+ str: strChunk,
337
+ token: '',
338
+ substr: strChunk,
339
+ });
340
+
341
+ var $1, $2, $3;
342
+ var count = 0;
343
+
344
+ while (true) {
345
+ if (data.state === null
346
+ && data.attrCapture !== undefined
347
+ && data.lexicalStack.frames.length === data.attrCaptureDepth) {
348
+ var $captured = result.splice(data.attrCapture).join('');
349
+ data.lexicalStack.current_frame().ast[AST_IDX_VAL] += $captured;
350
+ data.attrCapture = undefined;
351
+ data.state = '(@ _';
352
+ }
353
+ if (!buf.substr) {
354
+ data.cursor += buf.cursor;
355
+ return;
356
+ }
357
+ // Code:
358
+ // #? : Possible escape sequence
359
+ // #_ : Acquire reader macro name
360
+ // #_[ : Acquire reader macro args
361
+ // #( : Opened trapdoor
362
+ // )#? : Closing trapdoor?
363
+ // (? : Discover purpose of opening bracket
364
+ // (# : Opening an s-exp for a tag
365
+ // (# ?: Discover what comes after tag operator
366
+ // (# (: Handle opener discovered within tag operand
367
+ // (@ ?: Discover what comes after attr operator
368
+ // (@1 : Handle first char of s-exp for attribute name
369
+ // (@! : Bad attribute
370
+ // (@_ : Acquire attribute name
371
+ // (@ ": Parsing an attribute value wrapped in double quotes
372
+ // (@ ': Parsing an attribute value wrapped in single quotes
373
+ // (@ _: Parsing an attribute value not wrapped in quotes
374
+ // (& : Parse special char
375
+ // (& 1: Parse the first operand of the special char function
376
+ // (& _: Parse the remaining operands of the special char function
377
+ // (& ): Resolve the special char function
378
+ // )) : Handle second closing bracket
379
+ switch (data.state) {
380
+ case '#?': {
381
+ if ('(' === buf.substr[0]) {
382
+ data.state = '#(';
383
+ } else if (';' === buf.substr[0]) {
384
+ data.state = '#;';
385
+ } else if (/[a-zA-Z0-9_<>=.!-]/.test(buf.substr[0])) {
386
+ data.readerMacroName = buf.substr[0];
387
+ data.state = '#_';
388
+ } else {
389
+ result[result.length] = '#';
390
+ data.state = null;
391
+ }
392
+ buf.step();
393
+ } break;
394
+ case '#_': {
395
+ var nameChars = (buf.substr.match(/^[a-zA-Z0-9_<>=?!.-]*/) || [''])[0];
396
+ data.readerMacroName += nameChars;
397
+ buf.step(nameChars.length);
398
+ if (!buf.substr) { return; }
399
+ if ('_' === data.readerMacroName[0]) {
400
+ log_parse_error({ msg: "Reader macro names cannot start with underscore: #" + data.readerMacroName, lineNumber: data.lineCount });
401
+ data.readerMacroName = '';
402
+ data.state = null;
403
+ } else if ('[' === buf.substr[0]) {
404
+ buf.step();
405
+ data.state = '#_[';
406
+ } else {
407
+ var reader = data.readers[data.readerMacroName] || data.readers._default;
408
+ reader(data, null);
409
+ data.readerMacroName = '';
410
+ data.state = null;
411
+ }
412
+ } break;
413
+ case '#_[': {
414
+ var idxClose = buf.substr.indexOf(']');
415
+ if (idxClose < 0) {
416
+ data.readerMacroArgs += buf.read_to_end();
417
+ return;
418
+ }
419
+ data.readerMacroArgs += buf.read_to(idxClose);
420
+ buf.step();
421
+ var reader = data.readers[data.readerMacroName] || data.readers._default;
422
+ reader(data, data.readerMacroArgs);
423
+ data.readerMacroName = '';
424
+ data.readerMacroArgs = '';
425
+ data.state = null;
426
+ } break;
427
+ case '#(': {
428
+ result[result.length] = buf.read_to(')')
429
+ if (!buf.substr) {
430
+ return;
431
+ } else {
432
+ buf.step();
433
+ data.state = ')#?';
434
+ }
435
+ } break;
436
+ case ')#?': {
437
+ if ('#' === buf.substr[0]) {
438
+ buf.step();
439
+ data.state = null;
440
+ } else {
441
+ result[result.length] = ')';
442
+ data.state = '#(';
443
+ }
444
+ } break;
445
+ case '#;': {
446
+ buf.skip_whitespace();
447
+ if (!buf.substr) { return; }
448
+ if ('(' === buf.substr[0]) {
449
+ data.commentDepth = 1;
450
+ buf.step();
451
+ data.state = '#;(';
452
+ } else {
453
+ data.state = null;
454
+ }
455
+ } break;
456
+ case '#;(': {
457
+ var i = 0;
458
+ var len = buf.substr.length;
459
+ while (i < len) {
460
+ if (buf.substr[i] === '(') {
461
+ data.commentDepth++;
462
+ } else if (buf.substr[i] === ')') {
463
+ data.commentDepth--;
464
+ if (data.commentDepth === 0) {
465
+ buf.reset(i);
466
+ buf.step();
467
+ data.state = null;
468
+ break;
469
+ }
470
+ }
471
+ i++;
472
+ }
473
+ if (data.state === '#;(') {
474
+ buf.reset(len);
475
+ return;
476
+ }
477
+ } break;
478
+ case '(?': {
479
+ if (!buf.substr) { return; }
480
+ $1 = identify_operator(buf.substr);
481
+ switch ($1) {
482
+ case 'tag':
483
+ $2 = data.lexicalStack.current_frame();
484
+ if (data.isPrettyFormatting && data.hadNewline && $2) {
485
+ $2.hasBlockChildren = true;
486
+ }
487
+ data.lexicalStack.push(iStackFrame.new({
488
+ processor: null,
489
+ operator: '',
490
+ ast: [''], // 1st element contains whitespace
491
+ namespace: ($2 || {}).namespace,
492
+ indent: data.currentLineIndent
493
+ }));
494
+ data.hadNewline = false;
495
+ data.state = '(#';
496
+ break;
497
+ case 'attr':
498
+ buf.step();
499
+ data.state = '(@1';
500
+ break;
501
+ case 'bracket':
502
+ data.lexicalStack.push(iStackFrame.new({
503
+ processor: data.processor,
504
+ operator: '('
505
+ }));
506
+ result[result.length] = '(';
507
+ buf.step();
508
+ data.state = null;
509
+ break;
510
+ case 'specialchar': {
511
+ var $c = buf.substr[0];
512
+ var $isDirect = $c === '<' || $c === '>';
513
+ data.lexicalStack.push(iStackFrame.new({
514
+ processor: (data.lexicalStack.current_frame() || {}).processor || data.processor,
515
+ operator: '&',
516
+ ast: $isDirect ? [$c, 1] : [''],
517
+ namespace: (
518
+ data.lexicalStack.current_frame() || {}
519
+ ).namespace,
520
+ }));
521
+ data.state = $isDirect ? '(& )' : '(&';
522
+ buf.step();
523
+ } break;
524
+ default:
525
+ result[result.length] = '(';
526
+ data.state = null;
527
+ // XXX Why don't we advance here?
528
+ // We must be advancing somewhere else?
529
+ // Is that ok?
530
+ }
531
+ } break;
532
+ case '(#': {
533
+ $1 = data.lexicalStack.current_frame();
534
+ $1.operator += buf.read_token();
535
+ $2 = $1.operator.indexOf(":");
536
+ if ($2 < 0) {
537
+ $1.processor = last_but(1, data.lexicalStack.frames, {}).processor || data.processor;
538
+ } else {
539
+ $1.processor = $1.operator.substring(0, $2);
540
+ if (!data.processors[$1.processor]) {
541
+ $1.processor = defaultProcessor;
542
+ }
543
+ }
544
+ var $parentFrame = last_but(1, data.lexicalStack.frames, {});
545
+ if ($parentFrame.isExtWrapper) {
546
+ $1.processor = $parentFrame.processor;
547
+ data.lexicalStack.frames.splice(data.lexicalStack.frames.length - 2, 1);
548
+ }
549
+ if (!buf.substr) { return; }
550
+ $1.ast[AST_IDX_TAG] = $1.operator;
551
+ $1.ast[AST_IDX_AT1] = [];
552
+ data.state = '(# ?';
553
+ } break;
554
+ case '(# (': {
555
+ switch (identify_operator(buf.substr)) {
556
+ case 'attr':
557
+ data.lexicalStack.push(iStackFrame.new({
558
+ processor: data.processor,
559
+ operator: '@',
560
+ ast: [data.barf_ws()],
561
+ namespace: (
562
+ data.lexicalStack.current_frame() || {}
563
+ ).namespace,
564
+ }));
565
+ $1 = data.lexicalStack.current_frame().ast;
566
+ buf.step();
567
+ data.state = '(@1';
568
+ break;
569
+ case 'tag':
570
+ $1 = data.lexicalStack.current_frame();
571
+ if (data.isPrettyFormatting && is_found(data.wsBuf, '\n')) {
572
+ $1.hasBlockChildren = true;
573
+ }
574
+ result[result.length] =
575
+ data.processors[$1.processor](
576
+ 'open-tag',
577
+ $1,
578
+ '',
579
+ data.processorState[$1.processor],
580
+ {
581
+ ast: $1.ast,
582
+ cursor: data.cursor + buf.cursor,
583
+ source: data.source,
584
+ lineCount: data.lineCount
585
+ }
586
+ );
587
+ data.lexicalStack.push(iStackFrame.new({
588
+ processor: null,
589
+ operator: '',
590
+ ast: [''],
591
+ namespace: $1.namespace
592
+ }));
593
+ data.lexicalStack.current_frame().ast[AST_IDX_WS] = (
594
+ is_found(data.wsBuf, '\n')? data.barf_ws() : data.barf_ws().substring(1)
595
+ );
596
+ data.lexicalStack.current_frame().indent =
597
+ (data.lexicalStack.current_frame().ast[AST_IDX_WS] || '').match(/[^\n]*$/)[0];
598
+ data.state = '(#';
599
+ break;
600
+ case 'bracket':
601
+ result[result.length] =
602
+ data.processors[data.lexicalStack.processor()](
603
+ 'open-tag',
604
+ data.lexicalStack.current_frame(),
605
+ data.processorState[data.lexicalStack.processor()],
606
+ {
607
+ cursor: data.cursor + buf.cursor,
608
+ source: data.source,
609
+ lineCount: data.lineCount
610
+ }
611
+ );
612
+ data.lexicalStack.push(iStackFrame.new({
613
+ processor: data.processor,
614
+ operator: '('
615
+ }));
616
+ result[result.length] =
617
+ data.processors[data.lexicalStack.processor()](
618
+ 'begin-escape-bracket',
619
+ data.lexicalStack.current_frame(),
620
+ null,
621
+ {
622
+ cursor: data.cursor + buf.cursor,
623
+ source: data.source,
624
+ lineCount: data.lineCount
625
+ }
626
+ );
627
+ buf.step();
628
+ data.wsBuf = '';
629
+ data.state = null;
630
+ break;
631
+ case 'specialchar': {
632
+ $1 = data.lexicalStack.current_frame();
633
+ result[result.length] = data.processors[$1.processor](
634
+ 'open-tag',
635
+ $1,
636
+ '',
637
+ data.processorState[$1.processor],
638
+ {
639
+ ast: $1.ast,
640
+ cursor: data.cursor + buf.cursor,
641
+ source: data.source,
642
+ lineCount: data.lineCount
643
+ }
644
+ );
645
+ data.barf_ws();
646
+ var $c2 = buf.substr[0];
647
+ var $isDirect2 = $c2 === '<' || $c2 === '>';
648
+ data.lexicalStack.push(iStackFrame.new({
649
+ processor: $1.processor || data.processor,
650
+ operator: '&',
651
+ ast: $isDirect2 ? [$c2, 1] : [''],
652
+ namespace: $1.namespace,
653
+ }));
654
+ data.state = $isDirect2 ? '(& )' : '(&';
655
+ buf.step();
656
+ } break;
657
+ case 'none':
658
+ $1 = data.lexicalStack.processor();
659
+ result[result.length] =
660
+ data.processors[$1](
661
+ 'open-tag',
662
+ data.lexicalStack.current_frame(),
663
+ data.processorState[data.lexicalStack.processor()],
664
+ {
665
+ ast: $1.ast,
666
+ cursor: data.cursor + buf.cursor,
667
+ source: data.source,
668
+ lineCount: data.lineCount
669
+ }
670
+ );
671
+ data.state = null;
672
+ buf.step();
673
+ break;
674
+ default: {
675
+ console.error(["ERR: ", identify_operator(buf.substr), "did not match a pattern"].join(' '));
676
+ }
677
+ }
678
+ } break;
679
+ case '(# ?': {
680
+ data.wsBuf += buf.read_whitespace();
681
+ $1 = data.lexicalStack.current_frame().ast
682
+ if (!buf.substr) { return; }
683
+ if (buf.substr[0] === ')') {
684
+ buf.step();
685
+ $1[AST_IDX_AT1] = [data.barf_ws()];
686
+ $2 = data.lexicalStack.pop(buf, true);
687
+ result[result.length] = $2.processedData;
688
+ data.state = null;
689
+ } else if (buf.substr[0] === '(') {
690
+ buf.step();
691
+ data.state = '(# (';
692
+ } else {
693
+ var $frame = data.lexicalStack.current_frame();
694
+ result[result.length] =
695
+ data.processors[$frame.processor](
696
+ 'open-tag',
697
+ $frame,
698
+ data.processorState[$frame.processor],
699
+ {
700
+ ast: $1,
701
+ cursor: data.cursor + buf.cursor,
702
+ source: data.source,
703
+ lineCount: data.lineCount
704
+ }
705
+ );
706
+ // TODO: Work out how to handle this whitespace after opening tag.
707
+ if (is_found(data.wsBuf, '\n')) {
708
+ result[result.length] = data.barf_ws();
709
+ } else {
710
+ result[result.length] = data.barf_ws().substring(1);
711
+ }
712
+ data.state = null;
713
+ }
714
+ } break;
715
+ case '(@1': {
716
+ $1 = buf.substr[0];
717
+ switch ($1) {
718
+ case ')':
719
+ buf.step();
720
+ data.state = null;
721
+ break;
722
+ default: {
723
+ if (/[\s'"]/.test($1)) {
724
+ data.state = '(@!';
725
+ } else {
726
+ buf.step();
727
+ if (!data.lexicalStack.current_frame()) {
728
+ data.lexicalStack.push(iStackFrame.new({
729
+ processor: data.processor,
730
+ operator: '@',
731
+ ast: []
732
+ }));
733
+ }
734
+ data.lexicalStack.current_frame().ast[AST_IDX_NAME] = $1;
735
+ data.state = '(@_';
736
+ }
737
+ }
738
+ }
739
+ } break;
740
+ case '(@!': {
741
+ // TODO: Put bad attributes into AST.
742
+ // Maybe by appending to ws string.
743
+ result[result.length] = buf.read_to(')')
744
+ if (!buf.substr) {
745
+ return;
746
+ }
747
+ buf.step();
748
+ data.state = null;
749
+ } break;
750
+ case '(@_': {
751
+ $1 = data.lexicalStack.current_frame();
752
+ $2 = $1.ast;
753
+ $2[AST_IDX_NAME] += buf.read_token();
754
+ if (!buf.substr) { return; }
755
+ if (!is_valid_attr_name($2[AST_IDX_NAME])) {
756
+ // XXX Will read funny for super-long attribute names
757
+ log_parse_error({
758
+ msg: "Invalid attribute name",
759
+ lineNumber: data.lineCount,
760
+ token: last($2)[AST_IDX_NAME]
761
+ });
762
+ }
763
+ $2[AST_IDX_VAL] = '';
764
+ data.state = '(@ ?';
765
+ } break;
766
+ case '(@ ?': {
767
+ buf.skip_whitespace();
768
+ if (!buf.substr) { return; }
769
+ $1 = buf.substr[0];
770
+ if (is_char_quote_mark($1)) {
771
+ data.lexicalStack.current_frame().ast[AST_IDX_VAL] = $1;
772
+ buf.step();
773
+ data.state = '(@ ' + $1;
774
+ } else if (')' === $1) {
775
+ data.lexicalStack.pop(buf);
776
+ buf.step();
777
+ data.state = '(# ?';
778
+ } else {
779
+ data.lexicalStack.current_frame().ast[AST_IDX_VAL] = '"';
780
+ buf.skip_whitespace();
781
+ data.state = '(@ _';
782
+ }
783
+ } break;
784
+ case '(@ "':
785
+ case "(@ '": {
786
+ $1 = index_of_closing_quote(buf.substr, last(data.state));
787
+ $2 = data.lexicalStack.current_frame().ast;
788
+ if ($1 > -1) {
789
+ $2[AST_IDX_VAL] += buf.read_to($1 +1);
790
+ if (!(0 === $1 && '\\' === (last(data.lastChunk)))) {
791
+ data.state = '(@ ?';
792
+ }
793
+ } else {
794
+ $2[AST_IDX_VAL] += buf.read_to_end();
795
+ return;
796
+ }
797
+ } break;
798
+
799
+ case '(@ ?(': {
800
+ if (!buf.substr) { return; }
801
+ if (buf.substr[0] === '@') {
802
+ data.lexicalStack.current_frame().ast[AST_IDX_VAL] += '(';
803
+ data.state = '(@ _';
804
+ } else {
805
+ data.attrCapture = result.length;
806
+ data.attrCaptureDepth = data.lexicalStack.frames.length;
807
+ data.state = '(?';
808
+ }
809
+ } break;
810
+
811
+ case '(@ _': {
812
+ $1 = buf.substr.indexOf(')');
813
+ $3 = buf.substr.indexOf('(');
814
+ $2 = data.lexicalStack.current_frame().ast;
815
+ if (definitely_comes_before($3, $1)) {
816
+ $2[AST_IDX_VAL] += buf.read_to($3);
817
+ buf.step();
818
+ data.state = '(@ ?(';
819
+ } else if ($1 > -1) {
820
+ $2[AST_IDX_VAL] += buf.read_to($1) + '"';
821
+ data.state = '(@ ?';
822
+ } else {
823
+ $2[AST_IDX_VAL] += buf.read_to_end();
824
+ return;
825
+ }
826
+ } break;
827
+
828
+ case '(&': {
829
+ $1 = data.lexicalStack.current_frame();
830
+ $1.ast[0] += buf.read_token();
831
+ if (!buf.substr) { return }
832
+ if (')' === buf.substr[0]) {
833
+ $1.ast[1] = 1;
834
+ data.state = '(& )';
835
+ } else {
836
+ $1.ast[1] = '';
837
+ buf.step();
838
+ data.state = '(& 1';
839
+ }
840
+ } break;
841
+
842
+ case '(& 1': {
843
+ $1 = data.lexicalStack.current_frame();
844
+ $1.ast[1] += buf.read_token()
845
+ if (!buf.substr) { return; }
846
+ $1.ast[1] = parseInt($1.ast[1], 10);
847
+ if (')' === buf.substr[0]) {
848
+ data.state = '(& )';
849
+ } else {
850
+ data.state = '(& _';
851
+ }
852
+ } break;
853
+
854
+ case '(& _': {
855
+ $1 = buf.substr.indexOf(')');
856
+ if ($1 < 0) { return; }
857
+ buf.reset($1);
858
+ data.state = '(& )';
859
+ } break;
860
+
861
+ case '(& )': {
862
+ $1 = data.lexicalStack.current_frame();
863
+ $2 = {'': '&', '&': '&', '<': '<', '>': '>'}[$1.ast[0]];
864
+ if ($2 !== undefined) {
865
+ result[result.length] = data.processors[data.lexicalStack.processor()](
866
+ 'content',
867
+ $1,
868
+ $2.repeat($1.ast[1]),
869
+ data.processorState[data.lexicalStack.processor()],
870
+ {
871
+ cursor: data.cursor + buf.cursor,
872
+ lineCount: data.lineCount,
873
+ source: data.source
874
+ }
875
+ );
876
+ } else {
877
+ $2 = data.processors[data.lexicalStack.processor()](
878
+ 'entity',
879
+ $1,
880
+ $1.ast[0],
881
+ data.processorState[data.lexicalStack.processor()],
882
+ {
883
+ cursor: data.cursor + buf.cursor,
884
+ lineCount: data.lineCount,
885
+ source: data.source
886
+ }
887
+ );
888
+ for ($3 = 0; $3 < $1.ast[1]; ++$3) {
889
+ result[result.length] = $2;
890
+ }
891
+ }
892
+ buf.step();
893
+ data.lexicalStack.pop(buf);
894
+ data.state = null;
895
+ } break;
896
+
897
+ case '))': {
898
+ if (!buf.substr.length) {
899
+ return;
900
+ }
901
+ if (')' === buf.substr[0]) {
902
+ buf.step();
903
+ }
904
+ data.state = null;
905
+ } break;
906
+
907
+ default: {
908
+ // Not quoting an attribute value. Any string that's
909
+ // within an s-exp is a text node (barring the operator).
910
+ //
911
+ // We can put anything up to a closing bracket straight
912
+ // to result if we're in a stack. Anything up to an opener
913
+ // if we're not.
914
+
915
+ var idxOpeningBracket = buf.substr.indexOf('(');
916
+ var idxClosingBracket = buf.substr.indexOf(')');
917
+ var idxHash = buf.substr.indexOf('#');
918
+
919
+ if (definitely_comes_before(idxClosingBracket, idxOpeningBracket)) {
920
+ $3 = buf.read_to(idxClosingBracket);
921
+ if (data.isPrettyFormatting) track_line_state($3, data);
922
+ result[result.length] = data.processors[data.lexicalStack.processor()](
923
+ 'content',
924
+ data.lexicalStack.current_frame(),
925
+ $3,
926
+ data.processorState[data.lexicalStack.processor()],
927
+ {
928
+ cursor: data.cursor + buf.cursor,
929
+ lineCount: data.lineCount,
930
+ source: data.source
931
+ }
932
+ );
933
+ if (data.lexicalStack.frames.length) {
934
+ if ('(' === data.lexicalStack.current_frame().operator) {
935
+ result[result.length] = data.processors[data.lexicalStack.processor()]('end-escape-bracket');
936
+ data.lexicalStack.pop(buf);
937
+ data.state = '))';
938
+ } else {
939
+ $1 = data.lexicalStack.pop(buf)
940
+ if ('tag' === identify_operator($1.operator)) {
941
+ if (data.isPrettyFormatting && $1.hasBlockChildren) {
942
+ $2 = result[result.length - 1] || '';
943
+ if (!$2.endsWith('\n')) {
944
+ result[result.length] = '\n' + ($1.indent || '');
945
+ }
946
+ }
947
+ result[result.length] = $1.processedData;
948
+ }
949
+ }
950
+ } else {
951
+ result[result.length] = ')';
952
+ }
953
+ buf.step();
954
+ continue;
955
+ }
956
+
957
+ if (definitely_comes_before(idxHash, idxOpeningBracket)) {
958
+ $3 = buf.read_to(idxHash);
959
+ if (data.isPrettyFormatting) track_line_state($3, data);
960
+ result[result.length] = data.processors[data.lexicalStack.processor()](
961
+ 'content',
962
+ data.lexicalStack.current_frame(),
963
+ $3,
964
+ data.processorState[data.lexicalStack.processor()],
965
+ {
966
+ cursor: data.cursor + buf.cursor,
967
+ lineCount: data.lineCount,
968
+ source: data.source
969
+ }
970
+ );
971
+ buf.step();
972
+ data.state = '#?';
973
+ continue;
974
+ }
975
+
976
+ if (idxOpeningBracket > -1) {
977
+ $3 = buf.read_to(idxOpeningBracket);
978
+ if (data.isPrettyFormatting) track_line_state($3, data);
979
+ result[result.length] = data.processors[data.lexicalStack.processor()](
980
+ 'content',
981
+ data.lexicalStack.current_frame(),
982
+ $3,
983
+ data.processorState[data.lexicalStack.processor()],
984
+ {
985
+ cursor: data.cursor + buf.cursor,
986
+ lineCount: data.lineCount,
987
+ source: data.source
988
+ }
989
+ );
990
+ buf.step();
991
+ data.state = '(?';
992
+ continue;
993
+ } else {
994
+ $3 = buf.read_to_end();
995
+ if (data.isPrettyFormatting) track_line_state($3, data);
996
+ result[result.length] = data.processors[data.lexicalStack.processor()](
997
+ 'content',
998
+ data.lexicalStack.current_frame(),
999
+ $3,
1000
+ data.processorState[data.lexicalStack.processor()],
1001
+ {
1002
+ cursor: data.cursor + buf.cursor,
1003
+ lineCount: data.lineCount,
1004
+ source: data.source
1005
+ }
1006
+ );
1007
+ return;
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ }