@longform/longform 0.0.18 → 0.0.20

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,230 @@ 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
- ;
255
-
256
- fragment.html += `<${element.tag}`
257
-
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]
232
+ const indent = m1[INDENT].length / 2;
233
+
234
+ // only one root fragment is allowed. Skip until beginning of next fragment / directive.
235
+ if (skipping && indent !== 0) continue;
236
+ // I hear avoiding branching is faster than avoiding assignments...
237
+ skipping = false;
238
+
239
+ // verbatim blocks collect the string as is as a continuous block
240
+ // and do processing on it once the full block has been collected.
241
+ if (verbatimType !== 0) {
242
+ if (indent >= verbatimIndent && (
243
+ // text verbatim type should exit when other Longform constructs begin.
244
+ verbatimType !== 1 ||
245
+ (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) === undefined
246
+ )) {
247
+ if (verbatimType === 1) {
248
+ // Text verbatim blocks join with blank spaces
249
+ verbatimText += ' ' + m1[0].trim();
270
250
  } 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}"`;
251
+ if (verbatimText !== '') verbatimText += '\n';
252
+
253
+ // other verbatim blocks normalize the Longform indent of the block away
254
+ verbatimText += m1[0].replace(' '.repeat(verbatimIndent), '');
282
255
  }
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;
256
+ continue;
257
+ } else if (m1[0].trim() === '' &&
258
+ input.length !== m1.index + m1[0].length) {
259
+ // blank line in verbatim are ignored by text types
260
+ if (verbatimType !== 1) verbatimText += '\n';
293
261
 
294
- for (let i = 0, l = element.chain.length; i < l; i++) {
295
- chained = element.chain[i];
262
+ continue;
263
+ } else {
264
+ // verbatim block is finished
265
+ switch (verbatimType) {
266
+ case 1:
267
+ // text
268
+ fragment.html += verbatimText;
269
+
270
+ // locate reference points in text
271
+ while ((m2 = refRe.exec(verbatimText))) {
272
+ const start = fragment.html.length + m2.index - verbatimText.length;
273
+
274
+ fragment.refs.push({
275
+ id: m2[1],
276
+ start,
277
+ end: start + m2[0].length,
278
+ });
279
+ }
296
280
 
297
- fragment.html += '<' + chained.tag;
281
+ applyIndent(indent, key, element, fragment, doc, parsed, output, args);
282
+ break
283
+ case 2:
284
+ // escaped preformatted text
285
+ fragment.html += escape(verbatimText);
298
286
 
299
- if (chained.id !== undefined) {
300
- fragment.html += ' id="' + chained.id + '"';
301
- }
287
+ break;
288
+ case 3:
289
+ // preformatted
290
+ fragment.html += verbatimText + '\n';
302
291
 
303
- if (chained.class != undefined) {
304
- fragment.html += ' class="' + chained.class + '"';
305
- }
292
+ break;
293
+ case 4:
294
+ // directive block args
295
+ if (directive.def == null) break;
296
+ if (doc == null) {
297
+ if (typeof directive.def.meta === 'function') {
298
+ meta[m1[DIRECTIVE]] = directive.def.meta({
299
+ inlineArgs: directive.inlineArgs,
300
+ blockArgs: verbatimText,
301
+ });
302
+ }
303
+ } else if (typeof directive.def.render === 'function') {
304
+ try {
305
+ element.html += directive.def.render({
306
+ doc,
307
+ inlineArgs: directive.inlineArgs,
308
+ blockArgs: verbatimText,
309
+ }) + ' ';
310
+ } catch (err) {
311
+ console.error(`Error in calling directive ${directive.name}`)
312
+ console.error(err)
313
+ }
314
+ } else if (typeof directive.def.asyncRender === 'function') {
315
+ asyncCount++;
316
+
317
+ // async rendering uses the #[ref] feature to insert the
318
+ // eventual response.
319
+ const directiveFragment = makeFragment('embed');
320
+
321
+ directiveFragment.id = `@${asyncCount}`;
322
+ parsed.set(directiveFragment.id, directiveFragment);
323
+
324
+ fragment.refs.push({
325
+ id: directiveFragment.id,
326
+ start: fragment.html.length,
327
+ end: fragment.html.length,
328
+ });
329
+
330
+ // in case text follows we want a space separating the render output
331
+ fragment.html += ' ';
332
+
333
+ promises.push(
334
+ directive.def.asyncRender({
335
+ doc,
336
+ inlineArgs: directive.inlineArgs,
337
+ blockArgs: verbatimText,
338
+ }).then(res => {
339
+ directiveFragment.html = res ?? '';
340
+ }).catch(err => {
341
+ console.error(`Error in calling directive ${directive.name}`)
342
+ console.error(err)
343
+ })
344
+ );
345
+ } else if (typeof directive.def.element === 'function') {
346
+ if (element.beforeRender == null) element.beforeRender = [];
306
347
 
307
- for (const attr of Object.entries(chained.attrs)) {
308
- if (attr[1] === undefined) {
309
- fragment.html += ' ' + attr[0]
348
+ element.beforeRender.push({
349
+ blockArgs: verbatimText,
350
+ inlineArgs: directive.inlineArgs,
351
+ element: directive.def.element,
352
+ });
310
353
  } else {
311
- fragment.html += ` ${attr[0]}="${attr[1]}"`;
354
+ console.warn(`Directive used in incorrect context ${directive.name}`);
312
355
  }
313
- }
314
-
315
- fragment.html += '>';
316
356
  }
317
- }
318
357
 
319
- if (!voids.has(element.tag as string) && element.text != null) {
320
- fragment.html += element.text;
321
- }
358
+ verbatimType = 0;
359
+ verbatimText = '';
322
360
 
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
- }
396
- } else {
397
- const line = m1[0].replace(' '.repeat(verbatimIndent + 1), '');
398
-
399
- if (element.tag != null) {
400
- applyIndent(indent as number);
401
- }
402
-
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;
361
+ if (preformatClose.has(m1[0].trim())) continue;
416
362
  }
417
363
  }
418
364
 
419
365
  if (m1[LINE].trim() === '') {
366
+ // empty lines have no effect from here on
420
367
  continue;
421
368
  }
422
369
 
423
- // The id and lang directives should proceed all other directives and
424
- // fragment declarations.
425
370
  if (doc === undefined) {
371
+ // The meta directives get special treatment.
372
+ // Parsers can use the head output type to output
373
+ // the meta results only.
374
+ let parseBlock = false;
426
375
  const inlineArgs = m1[DIRECTIVE_INLINE_ARGS] ?? '';
427
-
376
+
428
377
  switch (m1[DIRECTIVE]) {
429
- case 'id': {
378
+ case 'id':
430
379
  const url = inlineArgs.trim();
380
+
381
+ parseBlock = true;
382
+
431
383
  try {
432
384
  id = id ?? new URL(url, args?.base).toString();
433
385
  } catch (err) {
434
386
  console.warn(
435
- `Invalid URL given to @id directive: ${url} base=${args.base}`
387
+ `Invalid URL given to @id directive: ${url} base=${args?.base}`
436
388
  );
437
389
  }
438
- continue main;
439
- }
440
- case 'lang': {
390
+ break;
391
+ case 'lang':
392
+ parseBlock = true;
441
393
  lang = lang ?? inlineArgs.trim();
442
- continue main;
443
- }
444
- case 'dir': {
394
+ break;
395
+ case 'dir':
396
+ parseBlock = true;
445
397
  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
- }
398
+ }
458
399
 
459
- meta[m1[DIRECTIVE]] = def.meta({ inlineArgs });
460
- continue main;
461
- }
400
+ // Even though the builtin meta directives do not support block args
401
+ // the args need to be parsed so they do not end up in the output.
402
+ const def = directives[m1[DIRECTIVE]];
403
+
404
+ if (typeof def?.meta === 'function'|| parseBlock) {
405
+ verbatimIndent = indent + 1;
406
+ verbatimType = 4;
407
+ directive = {
408
+ name: m1[DIRECTIVE],
409
+ inlineArgs,
410
+ def,
462
411
  }
463
- }
464
412
 
465
- doc = new Doc(id, lang, dir, meta, args?.allowAll, args?.allowedAttributes, args?.allowedElements);
413
+ continue main;
414
+ }
466
415
 
467
- if (args?.outputMode === 'meta') {
416
+ if (args?.outputMode === 'head') {
468
417
  return {
469
418
  key,
470
419
  id,
471
420
  lang,
472
421
  dir,
473
- meta: doc.meta,
474
- };
422
+ meta,
423
+ data,
424
+ } as unknown as ParsedResult;
475
425
  }
426
+
427
+ doc = new Doc(id, lang, dir, meta, args?.allowAll, args?.allowedAttributes, args?.allowedElements);
476
428
  }
477
429
 
478
430
  switch (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) {
479
431
  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
- }
432
+ case '##':
433
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
489
434
 
490
435
  fragment.id = m1[ID];
491
436
 
@@ -505,90 +450,76 @@ export function longform(
505
450
  }
506
451
 
507
452
  break;
508
- }
509
- case '@': {
510
- const indent = m1[INDENT].length / 2;
511
-
512
- if (element.tag != null || textIndent != null) {
513
- applyIndent(indent);
453
+ case '@':
454
+ if (element.tag !== undefined) {
455
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
514
456
  }
515
457
 
458
+ const inlineArgs = m1[DIRECTIVE_INLINE_ARGS] ?? ''
459
+
516
460
  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;
461
+ case 'template':
462
+ if (indent === 0) {
463
+ let indented = false;
464
+ fragment.template = true;
465
+
466
+ templateLinesRe.lastIndex = sniffTestRe.lastIndex;
467
+ while ((m2 = templateLinesRe.exec(input))) {
468
+ if (m2[1] == null && !indented && fragment.id == null) {
469
+ m3 = idRe.exec(m2[0]);
470
+
471
+ if (m3 != null) fragment.id = m3[3];
472
+
473
+ fragment.html += m2[0];
474
+ } else if (m2[1] == null && indented) {
475
+ sniffTestRe.lastIndex = templateLinesRe.lastIndex - m2[0].length;
476
+ break;
477
+ } else {
478
+ fragment.html += '\n' + m2[0];
479
+ if (m2[1] != null) indented = true;
480
+ }
546
481
  }
482
+
483
+ [element, fragment] = applyIndent(0, key, element, fragment, doc, parsed, output, args);
547
484
  }
548
485
 
549
- applyIndent(0);
486
+ continue main;
487
+ case 'doctype':
488
+ fragment.html += `<!doctype ${(inlineArgs.trim() || 'html').trim()}>`;
550
489
  break;
551
- }
552
- case 'mount': {
553
- if (m1[DIRECTIVE_INLINE_ARGS] == null) {
554
- throw new Error('Mount points must have a name');
490
+ case 'xml':
491
+ fragment.html += `<?xml ${inlineArgs.trim() || 'version="1.0" encoding="UTF-8"'}?>`;
492
+ break;
493
+ case 'mount':
494
+ if (args?.outputMode !== 'mountable') break;
495
+
496
+ if (inlineArgs === '') {
497
+ console.warn('Mount points must have a name');
555
498
  } else if (fragment.type !== 'root') {
556
- throw new Error('Mounting is only allowed on a root element');
499
+ console.warn('Mounting is only allowed on a root element');
557
500
  }
558
501
 
559
502
  fragment.mountable = true;
560
- element.mount = m1[DIRECTIVE_INLINE_ARGS].trim();
503
+ element.mount = inlineArgs.trim();
561
504
  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
505
  }
585
506
 
507
+ const def = directives[m1[DIRECTIVE]];
508
+
509
+ // A directive may not be defined but we want to process
510
+ // any block args to keep the output valid. Builtin directives
511
+ // will be ignored unless they require block args.
512
+ verbatimIndent = indent + 1;
513
+ verbatimType = 4;
514
+ directive = {
515
+ name: m1[DIRECTIVE],
516
+ inlineArgs: m1[DIRECTIVE_INLINE_ARGS],
517
+ def,
518
+ };
519
+
586
520
  break;
587
- }
588
- case '[':
589
- case '::': {
521
+ case '::':
590
522
  if (m1[ELEMENT] !== undefined) {
591
- const indent = (m1[INDENT]?.length ?? 0) / 2;
592
523
  let preformattedType: string | undefined;
593
524
  let inlineText: string | undefined;
594
525
 
@@ -596,12 +527,10 @@ export function longform(
596
527
  element.tag !== undefined ||
597
528
  element.indent > indent
598
529
  ) {
599
- applyIndent(indent);
530
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
600
531
  }
601
532
 
602
533
  element.indent = indent;
603
-
604
- textIndent = null;
605
534
 
606
535
  if (indent === 0 && fragment.id == null) {
607
536
  if (root != null) {
@@ -615,12 +544,11 @@ export function longform(
615
544
  }
616
545
 
617
546
  const parent = element;
618
- outer.lastIndex = 0;
619
-
547
+ outerRe.lastIndex = 0;
620
548
 
621
549
  // Looping through chained element declarations
622
550
  // foo#x.y::bar::free::
623
- while ((m2 = outer.exec(m1[LINE]))) {
551
+ while ((m2 = outerRe.exec(m1[LINE]))) {
624
552
  let working: WorkingElement;
625
553
 
626
554
  preformattedType = m2[3];
@@ -636,9 +564,9 @@ export function longform(
636
564
  parent.chain.push(working);
637
565
  }
638
566
 
639
- inner.lastIndex = 0;
567
+ innerRe.lastIndex = 0;
640
568
  // Looping through ids, classes and attrs
641
- while((m3 = inner.exec(m2[2]))) {
569
+ while((m3 = innerRe.exec(m2[2]))) {
642
570
  if (m3[2] !== undefined) {
643
571
  working.id = m3[2];
644
572
  } else if (m3[1] !== undefined) {
@@ -649,12 +577,12 @@ export function longform(
649
577
  }
650
578
  } else {
651
579
  // TODO: Preserve quoting style around attribute values
652
- let value = m3[4] ?? m3[5] ?? m3[6];
580
+ let value: string | boolean | undefined | null = m3[4] ?? m3[5] ?? m3[6];
653
581
 
654
582
  // attribute directives
655
583
  if (value[0] === '@') {
656
- directiveAttr.lastIndex = 0;
657
- m4 = directiveAttr.exec(value);
584
+ attributeDirectiveRe.lastIndex = 0;
585
+ m4 = attributeDirectiveRe.exec(value);
658
586
 
659
587
  if (m4 != null) {
660
588
  const def = directives[m4[1]];
@@ -671,19 +599,19 @@ export function longform(
671
599
 
672
600
  switch (m3[3]) {
673
601
  case 'id':
674
- if (!working.id) {
602
+ if (!working.id && typeof value === 'string') {
675
603
  working.id = value;
676
604
  }
677
605
  break;
678
606
  case 'class':
679
- if (!working.class) {
607
+ if (!working.class && typeof value === 'string') {
680
608
  working.class = value;
681
- } else {
609
+ } else if (typeof value === 'string') {
682
610
  working.class += ' ' + value;
683
611
  }
684
612
  break;
685
613
  default:
686
- working.attrs[m3[3]] = value;
614
+ if (value !== false) working.attrs[m3[3]] = value;
687
615
  }
688
616
  }
689
617
  }
@@ -694,31 +622,39 @@ export function longform(
694
622
  // server specific process.
695
623
  if (element.mount != null) {
696
624
  const id = element.mount;
697
- applyIndent(indent + 1);
625
+
626
+ [element, fragment] = applyIndent(indent + 1, key, element, fragment, doc, parsed, output, args);
627
+
628
+ if (fragment.mountPoints == null) fragment.mountPoints = [];
629
+
698
630
  fragment.mountPoints.push({
699
631
  id,
700
632
  part: fragment.html,
701
633
  });
634
+
702
635
  fragment.html = '';
703
- applyIndent(indent);
636
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
704
637
  break;
705
638
  }
706
639
 
707
640
  if (preformattedType !== undefined) {
708
- verbatimFirst = true;
709
- verbatimIndent = indent;
710
- verbatimSerialize = preformattedType === '{{';
641
+ if (element.tag !== undefined)
642
+ [element, fragment] = applyIndent(indent + 1, key, element, fragment, doc, parsed, output, args);
643
+
644
+ verbatimIndent = indent + 1;
645
+ verbatimType = preformattedType === '{{' ? 3 : 2
711
646
  } else if (inlineText !== undefined) {
712
647
  element.text = inlineText;
713
648
  }
714
649
 
715
650
  break;
716
651
  }
717
-
718
- attribute1.lastIndex = 0;
652
+ case '[':
653
+ // TODO: Add attr directive support.
654
+ attributeRe.lastIndex = 0;
719
655
  m2 = m1[ATTR] !== undefined
720
- ? attribute1.exec(m1[LINE])
721
- : undefined;
656
+ ? attributeRe.exec(m1[LINE])
657
+ : null;
722
658
 
723
659
  if (m2 != null && element.tag != null) {
724
660
  if (m2[2] === 'id') {
@@ -739,84 +675,30 @@ export function longform(
739
675
 
740
676
  break;
741
677
  }
742
- }
743
- default: {
744
- m2 = text1.exec(m1[0]) as RegExpExecArray;
678
+ default:
679
+ if (element.tag !== undefined)
680
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
745
681
 
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
- }
682
+ verbatimText = m1[0].trim();
683
+ verbatimIndent = indent;
684
+ verbatimType = 1;
776
685
  }
777
686
  }
778
687
 
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
- }
688
+ applyIndent(0, key, element, fragment, doc, parsed, output, args);
806
689
 
807
- fragment.refs = [];
808
-
809
- return fragment;
810
- }
690
+ if (promises.length > 0) await Promise.all(promises);
811
691
 
812
692
  if (root?.mountable) {
813
693
  output.mountable = true;
814
694
  output.tail = root.html;
815
- output.mountPoints = root.mountPoints;
695
+ output.mountPoints = root.mountPoints ?? [];
816
696
 
817
697
  return output;
818
698
  }
819
699
 
700
+ const arr = Array.from(parsed.values());
701
+
820
702
  for (let i = 0; i < parsed.size + 1; i++) {
821
703
  let fragment: WorkingFragment;
822
704
 
@@ -832,38 +714,36 @@ export function longform(
832
714
  continue;
833
715
  }
834
716
 
835
- flatten(fragment)
717
+ flatten(fragment, claimed, parsed);
836
718
  }
837
-
719
+
838
720
  if (root?.html != null) {
839
721
  output.root = root.html;
840
722
  output.selector = `[data-${key}-root]`;
841
723
  }
842
724
 
843
- for (let i = 0; i < arr.length; i++) {
725
+ for (let i = 0, l = arr.length, f = arr[i]; i < l; i++) {
844
726
  let selector!: string;
845
- const fragment: WorkingFragment = arr[i];
846
727
 
847
- if (fragment == null || claimed.has(fragment.id as string)) {
728
+ if (f == null || claimed.has(f.id as string)) {
848
729
  continue;
849
730
  }
850
731
 
851
- switch (fragment.type) {
852
- case 'embed': {
732
+ switch (f.type) {
733
+ case 'embed':
853
734
  if (args?.predictable) {
854
- selector = `[id=${fragment.id}]`;
735
+ selector = `[id=${f.id}]`;
855
736
  break;
856
737
  }
857
- }
858
738
  case 'bare':
859
- case 'range': selector = `[data-${key}=${fragment.id}]`;
739
+ case 'range': selector = `[data-${key}=${f.id}]`;
860
740
  }
861
741
 
862
- output.fragments[fragment.id as string] = {
863
- id: fragment.id as string,
742
+ output.fragments[f.id as string] = {
743
+ id: f.id as string,
864
744
  selector,
865
- type: fragment.type as 'embed' | 'bare' | 'range',
866
- html: fragment.html,
745
+ type: f.type as 'embed' | 'bare' | 'range',
746
+ html: f.html,
867
747
  };
868
748
  }
869
749
 
@@ -884,13 +764,14 @@ export function longform(
884
764
  * @param getFragment - A function which returns an already processed fragment's HTML string.
885
765
  * @returns The processed template.
886
766
  */
887
- export function processTemplate(
767
+ export async function processTemplate(
888
768
  template: string,
889
769
  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) {
770
+ longformArgs?: LongformArgs,
771
+ getFragment?: (fragment: string) => string | undefined,
772
+ ): Promise<string | undefined> {
773
+ const lf = template.replace(templateRe, (_line, _structured, param, ref) => {
774
+ if (ref !== undefined && typeof getFragment === 'function') {
894
775
  const fragment = getFragment(ref);
895
776
 
896
777
  if (fragment == null) return '';
@@ -901,6 +782,206 @@ export function processTemplate(
901
782
  return args[param] != null ? escape(args[param].toString()) : '';
902
783
  });
903
784
 
904
- return Object.values(longform(lf).fragments)[0]?.html ?? null;
785
+ return Object.values((await longform(lf, longformArgs)).fragments)[0]?.html ?? null;
905
786
  }
906
787
 
788
+ /**
789
+ * Closes any current in progress element definition
790
+ * and creates a new working element.
791
+ */
792
+ function applyIndent(
793
+ targetIndent: number,
794
+ key: string,
795
+ element: WorkingElement,
796
+ fragment: WorkingFragment,
797
+ doc: Doc,
798
+ parsed: Map<string, WorkingFragment>,
799
+ output: ParsedResult,
800
+ args?: LongformArgs,
801
+ ): [element: WorkingElement, fragment: WorkingFragment] {
802
+ if (element.tag !== undefined) {
803
+ if (Array.isArray(element.beforeRender)) {
804
+ const el: Element = {
805
+ id: element.id,
806
+ tag: element.tag,
807
+ class: element.class,
808
+ attrs: element.attrs,
809
+ };
810
+ const chain: Element[] = [];
811
+
812
+ for (let i = 0, l = element.chain.length, el = element.chain[i]; i < l; i++) {
813
+ chain.push({
814
+ id: el.id,
815
+ tag: el.tag,
816
+ class: el.class,
817
+ attrs: el.attrs,
818
+ });
819
+ }
820
+
821
+ for (let i = 0, l = element.beforeRender.length, def = element.beforeRender[i]; i < l; i++) {
822
+ (def.element as (ctx: ElementCtx) => void)({
823
+ el,
824
+ chain,
825
+ doc,
826
+ inlineArgs: def.inlineArgs,
827
+ blockArgs: def.blockArgs,
828
+ });
829
+ }
830
+ }
831
+
832
+ const root = fragment.type === 'range'
833
+ ? targetIndent < 2
834
+ : fragment.html === ''
835
+ ;
836
+
837
+ fragment.html += `<${element.tag}`;
838
+
839
+ if (element.id !== undefined) {
840
+ fragment.html += ' id="' + element.id + '"';
841
+ }
842
+
843
+ if (element.class !== undefined) {
844
+ fragment.html += ' class="' + element.class + '"';
845
+ }
846
+
847
+ for (const attr of Object.entries(element.attrs)) {
848
+ if (attr[0] === 'id' || attr[0] === 'class') continue;
849
+ if (attr[1] == null) {
850
+ fragment.html += ' ' + attr[0]
851
+ } else {
852
+ fragment.html += ` ${attr[0]}="${attr[1]}"`;
853
+ }
854
+ }
855
+
856
+ if (root) {
857
+ if (fragment.type === 'root') {
858
+ fragment.html += ` data-${key}-root`;
859
+ } else if (fragment.type === 'bare' || fragment.type === 'range') {
860
+ fragment.html += ` data-${key}="${fragment.id}"`;
861
+ } else if (fragment.type === 'embed' && !args?.predictable) {
862
+ fragment.html += ` data-${key}="${fragment.id}"`;
863
+ }
864
+ }
865
+
866
+ if (element.mount !== undefined) {
867
+ fragment.html += ` data-${key}-mount="${element.mount}"`;
868
+ }
869
+
870
+ fragment.html += '>';
871
+
872
+ if (Array.isArray(element.chain)) {
873
+ let chained: WorkingElement;
874
+
875
+ for (let i = 0, l = element.chain.length; i < l; i++) {
876
+ chained = element.chain[i];
877
+
878
+ fragment.html += '<' + chained.tag;
879
+
880
+ if (chained.id !== undefined) {
881
+ fragment.html += ' id="' + chained.id + '"';
882
+ }
883
+
884
+ if (chained.class != undefined) {
885
+ fragment.html += ' class="' + chained.class + '"';
886
+ }
887
+
888
+ for (const attr of Object.entries(chained.attrs)) {
889
+ if (attr[1] === undefined) {
890
+ fragment.html += ' ' + attr[0]
891
+ } else {
892
+ fragment.html += ` ${attr[0]}="${attr[1]}"`;
893
+ }
894
+ }
895
+
896
+ fragment.html += '>';
897
+ }
898
+ }
899
+
900
+ if (!voids.has(element.tag as string) && element.text != null) {
901
+ fragment.html += element.text;
902
+ }
903
+
904
+ if (
905
+ !voids.has(element.tag as string)
906
+ ) {
907
+ fragment.els.push(element);
908
+ }
909
+ }
910
+
911
+ if (targetIndent <= element.indent) {
912
+ element = makeElement(targetIndent);
913
+
914
+ while (
915
+ fragment.els.length !== 0 && (
916
+ targetIndent == null ||
917
+ fragment.els[fragment.els.length - 1].indent !== targetIndent - 1
918
+ )
919
+ ) {
920
+ const element = fragment.els.pop() as WorkingElement;
921
+
922
+ if (Array.isArray(element.chain)) {
923
+ for (let i = 0, l = element.chain.length; i < l; i++) {
924
+ fragment.html += `</${element.chain[i].tag}>`;
925
+ }
926
+ }
927
+
928
+ fragment.html += `</${element?.tag}>`;
929
+ }
930
+
931
+ if (targetIndent === 0) {
932
+ if (fragment.template) {
933
+ output.templates[fragment.id] = fragment.html;
934
+ } else if (fragment.type !== 'root') {
935
+ parsed.set(fragment.id, fragment);
936
+ }
937
+
938
+ fragment = makeFragment();
939
+ }
940
+ } else {
941
+ element = makeElement(targetIndent)
942
+ }
943
+
944
+ return [element, fragment];
945
+ }
946
+
947
+ function flatten(
948
+ fragment: WorkingFragment,
949
+ claimed: Set<string>,
950
+ parsed: Map<string, WorkingFragment>,
951
+ locals: Set<string> = new Set(),
952
+ ): WorkingFragment {
953
+ let r: FragmentRef;
954
+ if (Array.isArray(fragment.refs)) {
955
+ for (let i = fragment.refs.length - 1; i > -1; i--) {
956
+ r = fragment.refs[i];
957
+
958
+ if (locals.has(r.id) || claimed.has(r.id) || !parsed.has(r.id)) {
959
+ // cannot use fragment here. Clear the reference marker
960
+ fragment.html = fragment.html.slice(0, r.start)
961
+ + fragment.html.slice(r.end)
962
+ } else {
963
+ locals.add(fragment.id);
964
+
965
+ const child = flatten(
966
+ parsed.get(r.id) as WorkingFragment,
967
+ claimed,
968
+ parsed,
969
+ locals,
970
+ );
971
+
972
+ fragment.html = fragment.html.slice(0, r.start)
973
+ + child.html
974
+ + fragment.html.slice(r.end);
975
+
976
+ if (child.type === 'embed') {
977
+ claimed.add(child.id)
978
+ }
979
+ }
980
+ }
981
+ }
982
+
983
+ // don't re-process the fragment
984
+ fragment.refs = undefined as unknown as FragmentRef[];
985
+
986
+ return fragment;
987
+ }