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