@longform/async 0.0.29

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.
@@ -0,0 +1,1045 @@
1
+ import type { Directive, Doc, Element, ElementCtx, FragmentRef, FragmentType, LongformArgs, OutputMode, ParsedLongform, TargetType, WorkingElement, WorkingFragment } from "./types.ts";
2
+
3
+ const LINE = 0
4
+ , INDENT = 1
5
+ , DIRECTIVE_KEY = 2
6
+ , DIRECTIVE = 3
7
+ , DIRECTIVE_INLINE_ARGS = 4
8
+ , ID_TYPE = 5
9
+ , ID = 6
10
+ , FRAGMENT_TYPE = 7
11
+ , ELEMENT = 8
12
+ , ATTR = 9
13
+ , COMMENT = 10
14
+ , sniffTestRe = /^((?: )*)(?:(@)([a-z][a-z\-]*(?::[a-z][a-z\-]*)?)(?:(?:(?::: (.*)))|(?:::)? *)?|(##?)([a-z][a-z\-]*)(?: ?(?: +([\["]))? *|(?: *))?|(?:[a-z][a-z\-]*(?::[a-z][a-z\-])?.*(::).*)|(?:(\[)[a-z][a-z\-]?.*(?:=.+)?\]\w*)|(--).*|(.+))$/gmi
15
+
16
+ // captures a single element definition which could be in a chain.
17
+ // id, class and attributes are matched as a single block for a later loop
18
+ // if in chained situation the regexp will need to be looped over for each element
19
+ // we know the element is the last if the final capture group has a positive match
20
+ // or if the line is consumed by the regexp.
21
+ , outerRe = /([a-z][\w\-]*(?::[a-z][\w\-]*)?)((?:(?:[^:])|(?::(?!:)))*)::(?: (?:({{?)|(.*)))?/gi
22
+
23
+ // captures each id, class and attribute declaration in an element
24
+ , innerRe = /(?:\.([a-z][\w\-]+)|#([a-z][\w\-]+)|\[([a-z][a-z\-]+(?::[a-z][a-z|\-]*)?)(?:=(?:"([^"]+)"|'([^']+)'|([^\]]+)))?\])/gi
25
+ , attributeRe = /((?:\ \ )+)\[(\w[\w-]*(?::\w[\w-]*)?)(?:=([^\n]+))?\]/
26
+ , attributeOrInlineDirectiveRe = /^\s*@([a-z][\w\-]*(?::[a-z][\w\-]*)?)\s*$/i
27
+ , idRe = /((?:\ \ )+)?#(#)?([\w\-]+)(?: ([\["]))?/i
28
+ , refRe = /#\[([\w\-]+)\]/g
29
+ , escapeRe = /([&<>"'#\[\]{}])/g
30
+ , templateLinesRe = /^(\ \ )?([^\n]+)$/gm
31
+ , templateRe = /#(#)?{([a-z][\w\-]*(?::[a-z][\w\-]*)?)}|#\[([a-z][\w\-]*(?::[a-z][\w\-]*)?)\]/g
32
+ , preformatClose = new Set(['}', '}}'])
33
+ , voids = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wrb']);
34
+
35
+
36
+ const entities = {
37
+ '&': '&amp;',
38
+ '<': '&lt;',
39
+ '>': '&gt;',
40
+ '"': '&quot;',
41
+ "'": '&apos;',
42
+ // '#': '&num;',
43
+ // '[': '&lbrak;',
44
+ // ']': '&rbrak;',
45
+ // '{': '&rbrace;',
46
+ // '}': '&lbrace;',
47
+ };
48
+
49
+ function escape(value: string): string {
50
+ return value.replace(escapeRe, (match) => {
51
+ return entities[match as keyof typeof entities] ?? match;
52
+ });
53
+ }
54
+
55
+ function makeElement(indent: number = 0): WorkingElement {
56
+ return {
57
+ indent,
58
+ key: undefined,
59
+ id: undefined,
60
+ tag: undefined as unknown as string,
61
+ class: undefined,
62
+ text: undefined,
63
+ attrs: {},
64
+ html: '',
65
+ mount: undefined,
66
+ chain: undefined as unknown as WorkingElement[],
67
+ beforeRender: undefined,
68
+ };
69
+ }
70
+
71
+ function makeFragment(type: FragmentType = 'bare'): WorkingFragment {
72
+ return {
73
+ id: undefined as unknown as string,
74
+ type,
75
+ html: '',
76
+ template: false,
77
+ mountable: false,
78
+ els: [],
79
+ refs: [],
80
+ mountPoints: [],
81
+ };
82
+ }
83
+
84
+ const directiveValidator = /^[a-z][a-z\-]*\:[a-z][a-z\-]*$/i;
85
+
86
+
87
+ /**
88
+ * Parses a longform document into a object containing the root and fragments
89
+ * in the output format.
90
+ *
91
+ * @param input - The Longform document to parse.
92
+ * @param args - Arguments for the Longform parser.
93
+ */
94
+ export async function longform(
95
+ input: string,
96
+ args?: LongformArgs,
97
+ ): Promise<ParsedLongform> {
98
+ let skipping: boolean = false
99
+ , verbatimText: string = ''
100
+ , verbatimIndent: number = 0
101
+ , verbatimType: number = 0
102
+ , lastIndex: number = 0
103
+ , element: WorkingElement = makeElement()
104
+ , fragment: WorkingFragment = makeFragment()
105
+ , directive!: {
106
+ name: string;
107
+ inlineArgs: string;
108
+ def?: Directive,
109
+ }
110
+ // the root fragment
111
+ , root: WorkingFragment | null = null
112
+ , id: string | undefined = args?.id
113
+ , type: string | undefined = args?.type
114
+ , lang: string | undefined = args?.lang
115
+ , dir: string | undefined = args?.dir
116
+ , doc!: Doc
117
+ , asyncCount: number = 0
118
+ , m1: RegExpExecArray | null = null
119
+ , m2: RegExpExecArray | null = null
120
+ , m3: RegExpExecArray | null = null
121
+ // ids of claimed fragments
122
+ const claimed: Set<string> = new Set()
123
+ , meta: Record<string, unknown> = Object.assign({}, args?.meta)
124
+ , data: Record<string, unknown> = {}
125
+ , outputMode: OutputMode = args?.outputMode ?? 'doc'
126
+ , targetType: TargetType = args?.targetType ?? 'text/html'
127
+ , parsed: Map<string, WorkingFragment> = new Map(
128
+ Object.entries(args?.fragments as unknown as Record<string, WorkingFragment> ?? {})
129
+ )
130
+ , output: ParsedLongform = { outputMode: args?.outputMode ?? 'doc' } as unknown as ParsedLongform
131
+ , directives: Record<string, Directive> = {
132
+ id: { attr: ctx => ctx.doc.id },
133
+ type: { attr: ctx => ctx.doc.type },
134
+ dir: { attr: ctx => ctx.doc.dir },
135
+ lang: { attr: ctx => ctx.doc.lang },
136
+ }
137
+ , promises: Array<Promise<void>> = []
138
+
139
+ let key: string | undefined = args?.key;
140
+
141
+ if (key == null && args?.annotations === 'obscure') {
142
+ const arr = new Uint8Array(10);
143
+
144
+ crypto.getRandomValues(arr);
145
+
146
+ // If this fails you are probably using it in an environment
147
+ // where `toHex()` is not supported. If this is a browser environment
148
+ // it is likely you do not need obscure data attributes. They
149
+ // are specifically for the purpose of safely server rendering
150
+ // fragments that the client application needs to identify.
151
+ //
152
+ // If that doesn't apply you can pass a key in the args that
153
+ // is cryptographically random generated along with the
154
+ // `'obscure'` annotation argument.
155
+ // deno-lint-ignore no-explicit-any
156
+ key = (arr as any).toHex().toLowerCase();
157
+ } else if (key == null && args?.annotations === 'predictable') {
158
+ key = 'lf';
159
+ }
160
+
161
+ output.fragments = {};
162
+ output.templates = {};
163
+
164
+ if (args?.directives != null) {
165
+ const entries = Object.entries(args.directives);
166
+
167
+ let e: [string, Directive];
168
+
169
+ for (let i = 0, l = entries.length; i < l; i++) {
170
+ e = entries[i];
171
+
172
+ if (!directiveValidator.test(e[0])) {
173
+ console.warn(`Invalid custom directive name '$e{[0]}'`);
174
+
175
+ continue;
176
+ }
177
+
178
+ directives[e[0]] = e[1];
179
+ }
180
+ }
181
+
182
+ // This is a hack to allow open verbatim blocks to detect
183
+ // when they close via an extra step in the loop.
184
+ input += '\n ';
185
+ sniffTestRe.lastIndex = 0;
186
+ main: while ((m1 = sniffTestRe.exec(input))) {
187
+ if (m1[COMMENT] === '--') {
188
+ continue;
189
+ } else if (fragment.template) {
190
+ fragment.html += m1[0];
191
+ }
192
+
193
+ const indent = m1[INDENT].length / 2;
194
+
195
+ // only one root fragment is allowed. Skip until beginning of next fragment / directive.
196
+ if (skipping && indent !== 0) continue;
197
+ // I hear avoiding branching is faster than avoiding assignments...
198
+ skipping = false;
199
+
200
+ // verbatim blocks collect the string as is as a continuous block
201
+ // and do processing on it once the full block has been collected.
202
+ if (verbatimType !== 0) {
203
+ if (indent >= verbatimIndent && (
204
+ // text verbatim type should exit when other Longform constructs begin.
205
+ verbatimType !== 1 ||
206
+ (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) === undefined
207
+ )) {
208
+ if (verbatimType === 1) {
209
+ // Text verbatim blocks join with blank spaces
210
+ verbatimText += ' ' + m1[0].trim();
211
+ } else {
212
+ if (verbatimText !== '') verbatimText += '\n';
213
+
214
+ // other verbatim blocks normalize the Longform indent of the block away
215
+ verbatimText += m1[0].replace(' '.repeat(verbatimIndent), '');
216
+ }
217
+ continue;
218
+ } else if (m1[0].trim() === '' &&
219
+ input.length !== m1.index + m1[0].length) {
220
+ // blank line in verbatim are ignored by text types
221
+ if (verbatimType !== 1) verbatimText += '\n';
222
+
223
+ continue;
224
+ } else {
225
+ // verbatim block is finished
226
+ switch (verbatimType) {
227
+ case 1:
228
+ // text
229
+ fragment.html += verbatimText;
230
+
231
+ // locate reference points in text
232
+ while ((m2 = refRe.exec(verbatimText))) {
233
+ const start = fragment.html.length + m2.index - verbatimText.length;
234
+
235
+ fragment.refs.push({
236
+ id: m2[1],
237
+ start,
238
+ end: start + m2[0].length,
239
+ });
240
+ }
241
+
242
+ break
243
+ case 2:
244
+ // escaped preformatted text
245
+ fragment.html += escape(verbatimText) + '\n';
246
+
247
+ break;
248
+ case 3:
249
+ // preformatted
250
+ fragment.html += verbatimText + '\n';
251
+
252
+ break;
253
+ case 4:
254
+ // directive block args
255
+ if (directive.def == null) break;
256
+ lastIndex = sniffTestRe.lastIndex;;
257
+ if (doc == null) {
258
+ if (typeof directive.def.meta === 'function') {
259
+ meta[directive.name] = directive.def.meta({
260
+ inlineArgs: directive.inlineArgs,
261
+ blockArgs: verbatimText,
262
+ });
263
+ }
264
+ } else if (typeof directive.def.render === 'function') {
265
+ try {
266
+ fragment.html += directive.def.render({
267
+ doc,
268
+ inlineArgs: directive.inlineArgs,
269
+ blockArgs: verbatimText,
270
+ }) ?? '' + ' ';
271
+ } catch (err) {
272
+ console.error(`Error in calling directive ${directive.name}`)
273
+ console.error(err)
274
+ }
275
+ } else if (typeof directive.def.asyncRender === 'function') {
276
+ asyncCount++;
277
+
278
+ // async rendering uses the #[ref] feature to insert the
279
+ // eventual response.
280
+ const name = directive.name;
281
+ const directiveFragment = makeFragment('embed');
282
+
283
+ directiveFragment.id = `@${asyncCount}`;
284
+ parsed.set(directiveFragment.id, directiveFragment);
285
+
286
+ fragment.refs.push({
287
+ id: directiveFragment.id,
288
+ start: fragment.html.length,
289
+ end: fragment.html.length,
290
+ });
291
+
292
+ // in case text follows we want a space separating the render output
293
+ fragment.html += ' ';
294
+
295
+ promises.push(
296
+ directive.def.asyncRender({
297
+ doc,
298
+ inlineArgs: directive.inlineArgs,
299
+ blockArgs: verbatimText,
300
+ }).then(res => {
301
+ directiveFragment.html = res ?? '';
302
+ }).catch(err => {
303
+ console.error(`Error in calling directive ${name}`)
304
+ console.error(err)
305
+ })
306
+ );
307
+ } else if (typeof directive.def.element === 'function') {
308
+ if (element.beforeRender == null) element.beforeRender = [];
309
+
310
+ element.beforeRender.push({
311
+ blockArgs: verbatimText,
312
+ inlineArgs: directive.inlineArgs,
313
+ element: directive.def.element,
314
+ });
315
+ } else {
316
+ console.warn(`Directive used in incorrect context ${directive.name}`);
317
+ }
318
+ sniffTestRe.lastIndex = lastIndex;
319
+ }
320
+
321
+ verbatimType = 0;
322
+ verbatimText = '';
323
+ applyIndent(indent, key, element, fragment, doc, parsed, output, args);
324
+
325
+ if (preformatClose.has(m1[0].trim())) continue;
326
+ }
327
+ }
328
+
329
+ if (m1[LINE].trim() === '') {
330
+ // empty lines have no effect from here on
331
+ continue;
332
+ }
333
+
334
+ if (doc === undefined) {
335
+ // The meta directives get special treatment.
336
+ // Parsers can use the head output type to output
337
+ // the meta results only.
338
+ let parseBlock = false, url: string;
339
+ const inlineArgs = m1[DIRECTIVE_INLINE_ARGS] ?? '';
340
+
341
+ switch (m1[DIRECTIVE]) {
342
+ case 'id':
343
+ url = inlineArgs.trim();
344
+ parseBlock = true;
345
+
346
+ try {
347
+ id = id ?? new URL(url).toString();
348
+ } catch {
349
+ console.warn(
350
+ `Invalid URL given to @id directive: ${url}`
351
+ );
352
+ }
353
+ break;
354
+ case 'type':
355
+ parseBlock = true;
356
+ type = type ?? inlineArgs.trim();
357
+ break;
358
+ case 'lang':
359
+ parseBlock = true;
360
+ lang = lang ?? inlineArgs.trim();
361
+ break;
362
+ case 'dir':
363
+ parseBlock = true;
364
+ dir = dir ?? inlineArgs.trim();
365
+ }
366
+
367
+ // Even though the builtin meta directives do not support block args
368
+ // the args need to be parsed so they do not end up in the output.
369
+ const def = directives[m1[DIRECTIVE]];
370
+
371
+ if (typeof def?.meta === 'function'|| parseBlock) {
372
+ verbatimIndent = indent + 1;
373
+ verbatimType = 4;
374
+ directive = {
375
+ name: m1[DIRECTIVE],
376
+ inlineArgs,
377
+ def,
378
+ }
379
+
380
+ continue main;
381
+ }
382
+
383
+ if (output.outputMode === 'head') {
384
+ return {
385
+ key,
386
+ id,
387
+ lang,
388
+ dir,
389
+ meta,
390
+ data,
391
+ } as unknown as ParsedLongform;
392
+ }
393
+
394
+ doc = Object.freeze({ id, type, lang, dir, meta });
395
+ }
396
+
397
+ switch (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) {
398
+ case '#':
399
+ case '##':
400
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
401
+
402
+ fragment.id = m1[ID];
403
+
404
+ if (indent === 0) {
405
+ if (m1[FRAGMENT_TYPE] == '[') {
406
+ fragment.type = 'range';
407
+ } else if (m1[FRAGMENT_TYPE] === '"') {
408
+ fragment.type = 'text';
409
+ } else if (m1[ID_TYPE] === '##') {
410
+ fragment.type = 'bare';
411
+ } else {
412
+ fragment.type = 'embed';
413
+ element.id = fragment.id;
414
+ }
415
+ } else {
416
+ element.id = fragment.id;
417
+ }
418
+
419
+ break;
420
+ // deno-lint-ignore no-fallthrough
421
+ case '::': {
422
+ if (
423
+ element.tag !== undefined ||
424
+ element.indent > indent
425
+ ) {
426
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
427
+ }
428
+
429
+ element.indent = indent;
430
+
431
+ if (indent === 0 && fragment.id == null) {
432
+ if (root != null) {
433
+ // skip if root is found and this fragment
434
+ // has no id
435
+ skipping = true;
436
+ } else {
437
+ fragment.type = 'root';
438
+ root = fragment;
439
+ }
440
+ }
441
+
442
+ // output mode templates parses fragments as though
443
+ // they were templates, including the root fragment
444
+ if (output.outputMode !== 'templates') {
445
+ let preformattedType: string | undefined;
446
+ let inlineText: string | undefined;
447
+
448
+ const parent = element;
449
+ outerRe.lastIndex = 0;
450
+
451
+ // Looping through chained element declarations
452
+ // foo#x.y::bar::free::
453
+ while ((m2 = outerRe.exec(m1[LINE]))) {
454
+ let working: WorkingElement;
455
+
456
+ preformattedType = m2[3];
457
+ inlineText = m2[4];
458
+
459
+ if (element.tag === undefined) {
460
+ element.tag = m2[1];
461
+ working = element;
462
+ } else {
463
+ if (parent.chain === undefined) parent.chain = [];
464
+ working = makeElement(indent);
465
+ working.tag = m2[1];
466
+ parent.chain.push(working);
467
+ }
468
+
469
+ innerRe.lastIndex = 0;
470
+ // Looping through ids, classes and attrs
471
+ while((m3 = innerRe.exec(m2[2]))) {
472
+ if (m3[2] !== undefined) {
473
+ working.id = m3[2];
474
+ } else if (m3[1] !== undefined) {
475
+ if (working.class == null) {
476
+ working.class = m3[1];
477
+ } else {
478
+ working.class += ' ' + m3[1];
479
+ }
480
+ } else {
481
+ // TODO: Preserve quoting style around attribute values
482
+ const value: string | boolean | undefined | null = m3[4] ?? m3[5] ?? m3[6];
483
+
484
+ handleAttribute(
485
+ m3[3],
486
+ value,
487
+ working,
488
+ doc,
489
+ directives,
490
+ );
491
+ }
492
+ }
493
+ }
494
+
495
+ if (preformattedType === undefined && inlineText != null) {
496
+ m2 = attributeOrInlineDirectiveRe.exec(inlineText);
497
+
498
+ if (m2 != null) {
499
+ const name = m2[1];
500
+ const def = directives[name];
501
+
502
+ if (typeof def?.render === 'function') {
503
+ inlineText = def.render({ doc }) ?? '';
504
+ } else if (typeof def?.asyncRender === 'function') {
505
+ asyncCount++;
506
+
507
+ // async rendering uses the #[ref] feature to insert the
508
+ // eventual response.
509
+ const directiveFragment = makeFragment('embed');
510
+
511
+ directiveFragment.id = `@${asyncCount}`;
512
+ parsed.set(directiveFragment.id, directiveFragment);
513
+
514
+ fragment.refs.push({
515
+ id: directiveFragment.id,
516
+ start: fragment.html.length,
517
+ end: fragment.html.length,
518
+ });
519
+
520
+ // in case text follows we want a space separating the render output
521
+ promises.push(
522
+ def.asyncRender({
523
+ doc,
524
+ }).then(res => {
525
+ directiveFragment.html = res ?? '';
526
+ }).catch(err => {
527
+ console.error(`Error in calling directive ${name}`)
528
+ console.error(err)
529
+ })
530
+ );
531
+ }
532
+ }
533
+ }
534
+
535
+ // this is a hack to get temp support of mounting
536
+ // working. In the future it will be moved to a
537
+ // server specific process.
538
+ if (element.mount != null) {
539
+ const id = element.mount;
540
+
541
+ [element, fragment] = applyIndent(indent + 1, key, element, fragment, doc, parsed, output, args);
542
+
543
+ if (fragment.mountPoints == null) fragment.mountPoints = [];
544
+
545
+ fragment.mountPoints.push({
546
+ id,
547
+ part: fragment.html,
548
+ });
549
+
550
+ fragment.html = '';
551
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
552
+ break;
553
+ }
554
+
555
+ if (preformattedType !== undefined) {
556
+ if (element.tag !== undefined)
557
+ [element, fragment] = applyIndent(indent + 1, key, element, fragment, doc, parsed, output, args);
558
+
559
+ verbatimIndent = indent + 1;
560
+ verbatimType = preformattedType === '{{' ? 3 : 2
561
+ } else if (inlineText !== undefined) {
562
+ element.text = inlineText;
563
+ }
564
+
565
+ break;
566
+ }
567
+
568
+ // handle as directive if output mode is templates
569
+ // the templateLinesRe directive will need to re-process this
570
+ // line so we rewind the sniffTestRe last index for it to copy
571
+ sniffTestRe.lastIndex = sniffTestRe.lastIndex - m1[0].length;
572
+ }
573
+ case '@': {
574
+ if (element.tag !== undefined) {
575
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
576
+ }
577
+
578
+ const inlineArgs = m1[DIRECTIVE_INLINE_ARGS] ?? ''
579
+
580
+ switch (m1[DIRECTIVE] ?? 'template') {
581
+ case 'template':
582
+ if (indent === 0) {
583
+ let indented = false;
584
+ fragment.template = true;
585
+
586
+ templateLinesRe.lastIndex = sniffTestRe.lastIndex;
587
+ while ((m2 = templateLinesRe.exec(input))) {
588
+ if (m2[1] == null && !indented && fragment.id == null) {
589
+ m3 = idRe.exec(m2[0]);
590
+
591
+ if (m3 != null) fragment.id = m3[3];
592
+
593
+ fragment.html += m2[0];
594
+ } else if (m2[1] == null && indented) {
595
+ sniffTestRe.lastIndex = templateLinesRe.lastIndex - m2[0].length;
596
+ break;
597
+ } else {
598
+ fragment.html += '\n' + m2[0];
599
+ if (m2[1] != null) indented = true;
600
+ }
601
+ }
602
+
603
+ [element, fragment] = applyIndent(0, key, element, fragment, doc, parsed, output, args);
604
+ }
605
+
606
+ continue main;
607
+ case 'doctype':
608
+ fragment.html += `<!doctype ${(inlineArgs.trim() || 'html').trim()}>`;
609
+ break;
610
+ case 'xml':
611
+ fragment.html += `<?xml ${inlineArgs.trim() || 'version="1.0" encoding="UTF-8"'}?>`;
612
+ break;
613
+ case 'mount':
614
+ if (output.outputMode !== 'mountable') break;
615
+
616
+ if (inlineArgs === '') {
617
+ console.warn('Mount points must have a name');
618
+ } else if (fragment.type !== 'root') {
619
+ console.warn('Mounting is only allowed on a root element');
620
+ }
621
+
622
+ fragment.mountable = true;
623
+ element.mount = inlineArgs.trim();
624
+ break;
625
+ }
626
+
627
+ const def = directives[m1[DIRECTIVE]];
628
+
629
+ // A directive may not be defined but we want to process
630
+ // any block args to keep the output valid. Builtin directives
631
+ // will be ignored unless they require block args.
632
+ verbatimIndent = indent + 1;
633
+ verbatimType = 4;
634
+ directive = {
635
+ name: m1[DIRECTIVE],
636
+ inlineArgs: m1[DIRECTIVE_INLINE_ARGS],
637
+ def,
638
+ };
639
+
640
+ break;
641
+ }
642
+ // deno-lint-ignore no-fallthrough
643
+ case '[': {
644
+ if (element?.tag != null) {
645
+ attributeRe.lastIndex = 0;
646
+ m2 = attributeRe.exec(m1[LINE]);
647
+
648
+ if (m2 != null && element.tag != null) {
649
+ handleAttribute(
650
+ m2[2],
651
+ m2[3],
652
+ element,
653
+ doc,
654
+ directives,
655
+ );
656
+
657
+ break;
658
+ }
659
+ }
660
+
661
+ // if the attribute is not valid fall through to text processing of the line
662
+ }
663
+ default:
664
+ if (element.tag !== undefined)
665
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
666
+
667
+ verbatimText = m1[0].trim();
668
+ verbatimIndent = indent;
669
+ verbatimType = 1;
670
+ }
671
+ }
672
+
673
+ applyIndent(0, key, element, fragment, doc, parsed, output, args);
674
+
675
+ if (promises.length > 0) await Promise.all(promises);
676
+
677
+ if (root?.mountable) {
678
+ output.tail = root.html;
679
+ output.mountpoints = root.mountPoints ?? [];
680
+
681
+ return output;
682
+ }
683
+
684
+ const arr = Array.from(parsed.values());
685
+
686
+ for (let i = 0; i < parsed.size + 1; i++) {
687
+ let fragment: WorkingFragment;
688
+
689
+ if (i === 0 && root == null) {
690
+ continue;
691
+ } else if (i === 0) {
692
+ fragment = root as WorkingFragment;
693
+ } else {
694
+ fragment = arr[i - 1];
695
+ }
696
+
697
+ if (fragment.refs == null || fragment.refs.length === 0) {
698
+ continue;
699
+ }
700
+
701
+ flatten(fragment, claimed, parsed);
702
+ }
703
+
704
+ if (root?.html != null) {
705
+ if (root.template) {
706
+ output.template = root.html;
707
+ } else {
708
+ output.root = root.html;
709
+
710
+ if (key != null) output.selector = `[data-${key}-root]`;
711
+ }
712
+ }
713
+
714
+ let f: WorkingFragment;
715
+ for (let i = 0, l = arr.length; i < l; i++) {
716
+ let selector: string = undefined as unknown as string;
717
+
718
+ f = arr[i];
719
+
720
+ if (f?.id == undefined ||
721
+ claimed.has(f.id as string)) {
722
+ continue;
723
+ }
724
+
725
+ if (key != null) {
726
+ switch (f.type) {
727
+ // deno-lint-ignore no-fallthrough
728
+ case 'embed':
729
+ if (args?.annotations === 'obscure') {
730
+ selector = `[id=${f.id}]`;
731
+ break;
732
+ }
733
+ case 'bare':
734
+ case 'range': selector = `[data-${key}=${f.id}]`;
735
+ }
736
+ }
737
+
738
+ output.fragments[f.id as string] = {
739
+ id: f.id as string,
740
+ selector,
741
+ type: f.type as 'embed' | 'bare' | 'range',
742
+ html: f.html,
743
+ };
744
+ }
745
+
746
+ output.key = key as string;
747
+ output.id = id;
748
+ output.type = type
749
+ output.lang = lang;
750
+ output.dir = dir;
751
+ output.meta = meta;
752
+ output.outputMode = outputMode;
753
+ output.targetType = targetType;
754
+
755
+ return output;
756
+ }
757
+
758
+ /**
759
+ * Processes a client side Longform template to HTML fragment string.
760
+ *
761
+ * @param fragment - The fragment identifier.
762
+ * @param args - A record of template arguments.
763
+ * @param getFragment - A function which returns an already processed fragment's HTML string.
764
+ * @returns The processed template.
765
+ */
766
+ export async function processTemplate(
767
+ template: string,
768
+ args: Record<string, string | number>,
769
+ longformArgs?: LongformArgs,
770
+ getFragment?: (fragment: string) => string | undefined,
771
+ ): Promise<string | undefined> {
772
+ const processed = template.replace(templateRe, (_line, _structured, param, ref) => {
773
+ if (ref !== undefined && typeof getFragment === 'function') {
774
+ const fragment = getFragment(ref);
775
+
776
+ if (fragment == null) return '';
777
+
778
+ return fragment;
779
+ }
780
+
781
+ return args[param] != null ? escape(args[param].toString()) : '';
782
+ });
783
+
784
+ return Object.values((await longform(processed, longformArgs)).fragments)[0]?.html ?? null;
785
+ }
786
+
787
+ function handleAttribute(
788
+ name: string,
789
+ value: string | boolean | null | undefined,
790
+ element: WorkingElement,
791
+ doc: Doc,
792
+ directives: Record<string, Directive>,
793
+ ): void {
794
+ let m: RegExpExecArray | null;
795
+
796
+ if (typeof value === 'string' && value[0] === '@') {
797
+ m = attributeOrInlineDirectiveRe.exec(value as string);
798
+
799
+ if (m != null) {
800
+ const def = directives[m[1]];
801
+
802
+ if (def != null && typeof def.attr === 'function') {
803
+ value = def.attr({ doc, tag: element.tag as string });
804
+
805
+ if (!value || value == null) return;
806
+ } else {
807
+ return;
808
+ }
809
+ }
810
+ }
811
+
812
+ switch (name) {
813
+ case 'id':
814
+ if (!element.id && typeof value === 'string') {
815
+ element.id = value;
816
+ }
817
+ break;
818
+ case 'class':
819
+ if (!element.class && typeof value === 'string') {
820
+ element.class = value;
821
+ } else if (typeof value === 'string') {
822
+ element.class += ' ' + value;
823
+ }
824
+ break;
825
+ default:
826
+ if (value !== false && value != null) {
827
+ if (typeof element.attrs[name] === 'string' &&
828
+ typeof value === 'string') {
829
+ element.attrs[name] += ' ' + value;
830
+ } else if (typeof value === 'string') {
831
+ element.attrs[name] = value;
832
+ } else if (value === true && element.attrs[name] == null) {
833
+ element.attrs[name] = true;
834
+ }
835
+ }
836
+ }
837
+ }
838
+
839
+ /**
840
+ * Closes any current in progress element definition
841
+ * and creates a new working element.
842
+ */
843
+ function applyIndent(
844
+ targetIndent: number,
845
+ key: string | undefined,
846
+ element: WorkingElement,
847
+ fragment: WorkingFragment,
848
+ doc: Doc,
849
+ parsed: Map<string, WorkingFragment>,
850
+ output: ParsedLongform,
851
+ args?: LongformArgs,
852
+ ): [element: WorkingElement, fragment: WorkingFragment] {
853
+
854
+ if (element.tag !== undefined) {
855
+ if (Array.isArray(element.beforeRender)) {
856
+ const el: Element = {
857
+ id: element.id,
858
+ tag: element.tag,
859
+ class: element.class,
860
+ attrs: element.attrs,
861
+ };
862
+ const chain: Element[] = [];
863
+
864
+ for (let i = 0, l = element.chain.length, el = element.chain[i]; i < l; i++) {
865
+ chain.push({
866
+ id: el.id,
867
+ tag: el.tag,
868
+ class: el.class,
869
+ attrs: el.attrs,
870
+ });
871
+ }
872
+
873
+ for (let i = 0, l = element.beforeRender.length, def = element.beforeRender[i]; i < l; i++) {
874
+ (def.element as (ctx: ElementCtx) => void)({
875
+ el,
876
+ chain,
877
+ doc,
878
+ inlineArgs: def.inlineArgs,
879
+ blockArgs: def.blockArgs,
880
+ });
881
+ }
882
+ }
883
+
884
+ const root = fragment.type === 'range'
885
+ ? targetIndent < 2
886
+ : fragment.html === ''
887
+ ;
888
+
889
+ fragment.html += `<${element.tag}`;
890
+
891
+ if (element.id !== undefined) {
892
+ fragment.html += ' id="' + element.id + '"';
893
+ }
894
+
895
+ if (element.class !== undefined) {
896
+ fragment.html += ' class="' + element.class + '"';
897
+ }
898
+
899
+ for (const attr of Object.entries(element.attrs)) {
900
+ if (attr[0] === 'id' || attr[0] === 'class') continue;
901
+ if (attr[1] == null) {
902
+ fragment.html += ' ' + attr[0]
903
+ } else {
904
+ fragment.html += ` ${attr[0]}="${attr[1]}"`;
905
+ }
906
+ }
907
+
908
+ if (key != null) {
909
+
910
+ if (root) {
911
+ if (fragment.type === 'root') {
912
+ fragment.html += ` data-${key}-root`;
913
+ } else if (fragment.type === 'bare' || fragment.type === 'range') {
914
+ fragment.html += ` data-${key}="${fragment.id}"`;
915
+ } else if (fragment.type === 'embed' && args?.annotations === 'obscure') {
916
+ fragment.html += ` data-${key}="${fragment.id}"`;
917
+ }
918
+ }
919
+
920
+ if (element.mount !== undefined) {
921
+ fragment.html += ` data-${key}-mount="${element.mount}"`;
922
+ }
923
+ }
924
+
925
+ fragment.html += '>';
926
+
927
+ if (Array.isArray(element.chain)) {
928
+ let chained: WorkingElement;
929
+
930
+ for (let i = 0, l = element.chain.length; i < l; i++) {
931
+ chained = element.chain[i];
932
+
933
+ fragment.html += '<' + chained.tag;
934
+
935
+ if (chained.id !== undefined) {
936
+ fragment.html += ' id="' + chained.id + '"';
937
+ }
938
+
939
+ if (chained.class != undefined) {
940
+ fragment.html += ' class="' + chained.class + '"';
941
+ }
942
+
943
+ for (const attr of Object.entries(chained.attrs)) {
944
+ if (attr[1] === undefined) {
945
+ fragment.html += ' ' + attr[0]
946
+ } else {
947
+ fragment.html += ` ${attr[0]}="${attr[1]}"`;
948
+ }
949
+ }
950
+
951
+ fragment.html += '>';
952
+ }
953
+ }
954
+
955
+ if (!voids.has(element.tag as string) && element.text != null) {
956
+ fragment.html += element.text;
957
+ }
958
+
959
+ if (
960
+ !voids.has(element.tag as string)
961
+ ) {
962
+ fragment.els.push(element);
963
+ }
964
+ }
965
+
966
+ if (targetIndent <= element.indent) {
967
+ element = makeElement(targetIndent);
968
+
969
+ while (
970
+ fragment.els.length !== 0 && (
971
+ targetIndent == null ||
972
+ fragment.els[fragment.els.length - 1].indent !== targetIndent - 1
973
+ )
974
+ ) {
975
+ const element = fragment.els.pop() as WorkingElement;
976
+
977
+ if (Array.isArray(element.chain)) {
978
+ for (let i = 0, l = element.chain.length; i < l; i++) {
979
+ fragment.html += `</${element.chain[i].tag}>`;
980
+ }
981
+ }
982
+
983
+ fragment.html += `</${element?.tag}>`;
984
+ }
985
+
986
+ if (targetIndent === 0) {
987
+ if (fragment.type !== 'root') {
988
+ if (fragment.template) {
989
+ output.templates[fragment.id] = fragment.html;
990
+ } else {
991
+ parsed.set(fragment.id, fragment);
992
+ }
993
+ }
994
+
995
+ fragment = makeFragment();
996
+ }
997
+ } else {
998
+ element = makeElement(targetIndent)
999
+ }
1000
+
1001
+ return [element, fragment];
1002
+ }
1003
+
1004
+ function flatten(
1005
+ fragment: WorkingFragment,
1006
+ claimed: Set<string>,
1007
+ parsed: Map<string, WorkingFragment>,
1008
+ locals: Set<string> = new Set(),
1009
+ ): WorkingFragment {
1010
+ let r: FragmentRef;
1011
+ if (Array.isArray(fragment.refs)) {
1012
+ for (let i = fragment.refs.length - 1; i > -1; i--) {
1013
+ r = fragment.refs[i];
1014
+
1015
+ if (locals.has(r.id) || claimed.has(r.id) || !parsed.has(r.id)) {
1016
+ // cannot use fragment here. Clear the reference marker
1017
+ fragment.html = fragment.html.slice(0, r.start)
1018
+ + fragment.html.slice(r.end)
1019
+ } else {
1020
+ locals.add(fragment.id);
1021
+
1022
+ const child = flatten(
1023
+ parsed.get(r.id) as WorkingFragment,
1024
+ claimed,
1025
+ parsed,
1026
+ locals,
1027
+ );
1028
+
1029
+ fragment.html = fragment.html.slice(0, r.start)
1030
+ + child.html
1031
+ + fragment.html.slice(r.end);
1032
+
1033
+ if (child.type === 'embed') {
1034
+ claimed.add(child.id)
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+
1040
+ // don't re-process the fragment
1041
+ fragment.refs = undefined as unknown as FragmentRef[];
1042
+
1043
+ return fragment;
1044
+ }
1045
+