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