@longform/longform 0.0.18 → 0.0.19

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