@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/dist/longform.js CHANGED
@@ -1,14 +1,13 @@
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;
2
- 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
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
3
2
  // captures a single element definition which could be in a chain.
4
3
  // id, class and attributes are matched as a single block for a later loop
5
4
  // if in chained situation the regexp will need to be looped over for each element
6
5
  // we know the element is the last if the final capture group has a positive match
7
6
  // or if the line is consumed by the regexp.
8
- , outer = /([a-z][\w\-]*(?::[a-z][\w\-]*)?)((?:(?:[^:])|(?::(?!:)))*)::(?: (?:({{?)|(.*)))?/gi
7
+ , outerRe = /([a-z][\w\-]*(?::[a-z][\w\-]*)?)((?:(?:[^:])|(?::(?!:)))*)::(?: (?:({{?)|(.*)))?/gi
9
8
  // captures each id, class and attribute declaration in an element
10
- , inner = /(?:\.([a-z][\w\-]+)|#([a-z][\w\-]+)|\[([a-z][a-z\-]+(?::[a-z][a-z|\-]*)?)(?:=(?:"([^"]+)"|'([^']+)'|([^\]]+)))?\])/gi, attribute1 = /((?:\ \ )+)\[(\w[\w-]*(?::\w[\w-]*)?)(?:=([^\n]+))?\]/, directiveAttr = /^@([a-z][\w\-]*(?::[a-z][\w\-]*)?)$/i, preformattedClose = /[ \t]*}}?[ \t]*/, id1 = /((?:\ \ )+)?#(#)?([\w\-]+)(?: ([\["]))?/gmi, identRe = /^(\ \ )+/, text1 = /^((?:\ \ )+)([^ \n][^\n]*)$/i, refRe = /#\[([\w\-]+)\]/g, escapeRe = /([&<>"'#\[\]{}])/g, templateLinesRe = /^(\ \ )?([^\n]+)$/gmi, templateRe = /\#\{(@)?([a-z][\w\-]*(?::[a-z][\w\-]*))}/g, voids = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wrb']);
11
- let m1, m2, m3, m4;
9
+ , innerRe = /(?:\.([a-z][\w\-]+)|#([a-z][\w\-]+)|\[([a-z][a-z\-]+(?::[a-z][a-z|\-]*)?)(?:=(?:"([^"]+)"|'([^']+)'|([^\]]+)))?\])/gi, attributeRe = /((?:\ \ )+)\[(\w[\w-]*(?::\w[\w-]*)?)(?:=([^\n]+))?\]/, attributeDirectiveRe = /^@([a-z][\w\-]*(?::[a-z][\w\-]*)?)$/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
+ let m1 = null, m2 = null, m3 = null, m4 = null;
12
11
  const entities = {
13
12
  '&': '&amp;',
14
13
  '<': '&lt;',
@@ -44,6 +43,7 @@ function makeElement(indent = 0) {
44
43
  }
45
44
  function makeFragment(type = 'bare') {
46
45
  return {
46
+ id: undefined,
47
47
  type,
48
48
  html: '',
49
49
  template: false,
@@ -99,10 +99,10 @@ const directiveValidator = /^[a-z][a-z\-]*\:[a-z][a-z\-]*$/i;
99
99
  * @param input - The Longform document to parse.
100
100
  * @param args - Arguments for the Longform parser.
101
101
  */
102
- function longform(input, args) {
103
- let textIndent = null, verbatimSerialize = true, verbatimIndent = null, verbatimFirst = false, element = makeElement(), fragment = makeFragment()
102
+ async function longform(input, args) {
103
+ let skipping = false, verbatimText = '', verbatimIndent = 0, verbatimType = 0, element = makeElement(), fragment = makeFragment(), directive
104
104
  // the root fragment
105
- , root = null, id = args?.id, lang = args?.lang, dir = args?.dir, meta = {}, doc;
105
+ , root = null, id = args?.id, lang = args?.lang, dir = args?.dir, meta = {}, data = {}, doc, asyncCount = 0;
106
106
  // ids of claimed fragments
107
107
  const claimed = new Set()
108
108
  // parsed fragments
@@ -110,7 +110,7 @@ function longform(input, args) {
110
110
  id: { attr: ctx => ctx.doc.id },
111
111
  dir: { attr: ctx => ctx.doc.dir },
112
112
  lang: { attr: ctx => ctx.doc.lang },
113
- };
113
+ }, promises = [];
114
114
  let key = args?.key;
115
115
  if (!args?.predictable && key == null) {
116
116
  const arr = new Uint8Array(10);
@@ -124,245 +124,200 @@ function longform(input, args) {
124
124
  output.templates = {};
125
125
  if (args?.directives != null) {
126
126
  const entries = Object.entries(args.directives);
127
- for (let i = 0, l = entries.length; i < l; i++) {
128
- if (!directiveValidator.test(entries[i][0])) {
129
- console.warn(`Invalid custom directive name '${entries[i][0]}'`);
127
+ for (let i = 0, l = entries.length, e = entries[i]; i < l; i++) {
128
+ if (!directiveValidator.test(e[0])) {
129
+ console.warn(`Invalid custom directive name '$e{[0]}'`);
130
130
  continue;
131
131
  }
132
- directives[entries[i][0]] = entries[i][1];
132
+ directives[e[0]] = e[1];
133
133
  }
134
134
  }
135
- /**
136
- * Closes any current in progress element definition
137
- * and creates a new working element.
138
- */
139
- function applyIndent(targetIndent) {
140
- if (Array.isArray(element.beforeRender)) {
141
- const el = {
142
- id: element.id,
143
- tag: element.tag,
144
- class: element.class,
145
- attrs: element.attrs,
146
- };
147
- const chain = [];
148
- for (let i = 0, l = element.chain.length, el = element.chain[i]; i < l; i++) {
149
- chain.push({
150
- id: el.id,
151
- tag: el.tag,
152
- class: el.class,
153
- attrs: el.attrs,
154
- });
155
- }
156
- for (let i = 0, l = element.beforeRender.length, def = element.beforeRender[i]; i < l; i++) {
157
- def.beforeRender({
158
- el,
159
- chain,
160
- doc: doc,
161
- inlineArg: def.inlineArg,
162
- blockArg: def.blockArg,
163
- });
164
- }
135
+ // This is a hack to allow open verbatim blocks to detect
136
+ // when they close via an extra step in the loop.
137
+ input += '\n ';
138
+ sniffTestRe.lastIndex = 0;
139
+ main: while ((m1 = sniffTestRe.exec(input))) {
140
+ if (m1[COMMENT] === '--') {
141
+ continue;
165
142
  }
166
- if (element.tag != null) {
167
- const root = fragment.type === 'range'
168
- ? targetIndent < 2
169
- : fragment.html === '';
170
- fragment.html += `<${element.tag}`;
171
- if (element.id !== undefined) {
172
- fragment.html += ' id="' + element.id + '"';
173
- }
174
- if (element.class !== undefined) {
175
- fragment.html += ' class="' + element.class + '"';
176
- }
177
- for (const attr of Object.entries(element.attrs)) {
178
- if (attr[0] === 'id' || attr[0] === 'class')
179
- continue;
180
- if (attr[1] == null) {
181
- fragment.html += ' ' + attr[0];
143
+ else if (fragment.template) {
144
+ fragment.html += m1[0];
145
+ }
146
+ const indent = m1[INDENT].length / 2;
147
+ // only one root fragment is allowed. Skip until beginning of next fragment / directive.
148
+ if (skipping && indent !== 0)
149
+ continue;
150
+ if (skipping)
151
+ skipping = false;
152
+ // verbatim blocks collect the string as is and do processing on it
153
+ // once the full block has been collected into one string
154
+ if (verbatimType !== 0) {
155
+ if (indent >= verbatimIndent && (verbatimType !== 1 || // text verbatim type should exit when other symbols are parsed
156
+ (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) === undefined)) {
157
+ // still in verbatim block
158
+ if (verbatimType === 1) {
159
+ verbatimText += ' ' + m1[0].trim();
182
160
  }
183
161
  else {
184
- fragment.html += ` ${attr[0]}="${attr[1]}"`;
185
- }
186
- }
187
- if (root) {
188
- if (fragment.type === 'root') {
189
- fragment.html += ` data-${key}-root`;
190
- }
191
- else if (fragment.type === 'bare' || fragment.type === 'range') {
192
- fragment.html += ` data-${key}="${fragment.id}"`;
193
- }
194
- else if (fragment.type === 'embed' && !args?.predictable) {
195
- fragment.html += ` data-${key}="${fragment.id}"`;
162
+ if (verbatimText !== '')
163
+ verbatimText += '\n';
164
+ verbatimText += m1[0].replace(m1[INDENT], '');
196
165
  }
166
+ continue;
197
167
  }
198
- if (element.mount !== undefined) {
199
- fragment.html += ` data-${key}-mount="${element.mount}"`;
168
+ else if (m1[0].trim() === '' &&
169
+ input.length !== m1.index + m1[0].length) {
170
+ // blank line in verbatim
171
+ if (verbatimType !== 1)
172
+ verbatimText += '\n';
173
+ continue;
200
174
  }
201
- fragment.html += '>';
202
- if (Array.isArray(element.chain)) {
203
- let chained;
204
- for (let i = 0, l = element.chain.length; i < l; i++) {
205
- chained = element.chain[i];
206
- fragment.html += '<' + chained.tag;
207
- if (chained.id !== undefined) {
208
- fragment.html += ' id="' + chained.id + '"';
209
- }
210
- if (chained.class != undefined) {
211
- fragment.html += ' class="' + chained.class + '"';
212
- }
213
- for (const attr of Object.entries(chained.attrs)) {
214
- if (attr[1] === undefined) {
215
- fragment.html += ' ' + attr[0];
175
+ else {
176
+ // verbatim block is finished
177
+ switch (verbatimType) {
178
+ case 1:
179
+ // text
180
+ fragment.html += verbatimText;
181
+ // locate reference points in text
182
+ while ((m2 = refRe.exec(verbatimText))) {
183
+ const start = fragment.html.length + m2.index - verbatimText.length;
184
+ fragment.refs.push({
185
+ id: m2[1],
186
+ start,
187
+ end: start + m2[0].length,
188
+ });
189
+ }
190
+ applyIndent(indent, key, element, fragment, doc, parsed, output, args);
191
+ break;
192
+ case 2:
193
+ // escaped preformatted text
194
+ fragment.html += escape(verbatimText);
195
+ break;
196
+ case 3:
197
+ // preformatted
198
+ fragment.html += verbatimText + '\n';
199
+ break;
200
+ case 4:
201
+ // directive block args
202
+ if (directive.def == null)
203
+ break;
204
+ if (doc == null) {
205
+ if (typeof directive.def.meta === 'function') {
206
+ meta[m1[DIRECTIVE]] = directive.def.meta({
207
+ inlineArgs: directive.inlineArgs,
208
+ blockArgs: verbatimText,
209
+ });
210
+ }
211
+ }
212
+ else if (typeof directive.def.render === 'function') {
213
+ try {
214
+ element.html += directive.def.render({
215
+ doc,
216
+ inlineArgs: directive.inlineArgs,
217
+ blockArgs: verbatimText,
218
+ });
219
+ }
220
+ catch (err) {
221
+ console.error(`Error in calling directive ${directive.name}`);
222
+ console.error(err);
223
+ }
224
+ }
225
+ else if (typeof directive.def.asyncRender === 'function') {
226
+ asyncCount++;
227
+ // async rendering uses the #[ref] feature to insert the
228
+ // eventual response.
229
+ const directiveFragment = makeFragment('embed');
230
+ directiveFragment.id = `@${asyncCount}`;
231
+ parsed.set(directiveFragment.id, directiveFragment);
232
+ fragment.refs.push({
233
+ id: directiveFragment.id,
234
+ start: fragment.html.length,
235
+ end: fragment.html.length,
236
+ });
237
+ promises.push(directive.def.asyncRender({
238
+ doc,
239
+ inlineArgs: directive.inlineArgs,
240
+ blockArgs: verbatimText,
241
+ }).then(res => {
242
+ directiveFragment.html = res ?? '';
243
+ }).catch(err => {
244
+ console.error(`Error in calling directive ${directive.name}`);
245
+ console.error(err);
246
+ }));
247
+ }
248
+ else if (typeof directive.def.element === 'function') {
249
+ if (element.beforeRender == null)
250
+ element.beforeRender = [];
251
+ element.beforeRender.push({
252
+ blockArgs: verbatimText,
253
+ inlineArgs: directive.inlineArgs,
254
+ element: directive.def.element,
255
+ });
216
256
  }
217
257
  else {
218
- fragment.html += ` ${attr[0]}="${attr[1]}"`;
258
+ console.warn(`Directive used in incorrect context ${directive.name}`);
219
259
  }
220
- }
221
- fragment.html += '>';
222
- }
223
- }
224
- if (!voids.has(element.tag) && element.text != null) {
225
- fragment.html += element.text;
226
- }
227
- if (!voids.has(element.tag)) {
228
- fragment.els.push(element);
229
- }
230
- }
231
- if (targetIndent <= element.indent) {
232
- element = makeElement(targetIndent);
233
- while (fragment.els.length !== 0 && (targetIndent == null ||
234
- fragment.els[fragment.els.length - 1].indent !== targetIndent - 1)) {
235
- const element = fragment.els.pop();
236
- if (Array.isArray(element.chain)) {
237
- for (let i = 0, l = element.chain.length; i < l; i++) {
238
- fragment.html += `</${element.chain[i].tag}>`;
239
- }
240
260
  }
241
- fragment.html += `</${element?.tag}>`;
242
- }
243
- if (targetIndent === 0) {
244
- if (fragment.template) {
245
- output.templates[fragment.id] = fragment.html;
246
- }
247
- else if (fragment.type === 'root') {
248
- root = fragment;
249
- }
250
- else {
251
- parsed.set(fragment.id, fragment);
252
- }
253
- fragment = makeFragment();
254
- }
255
- }
256
- else {
257
- element = makeElement(targetIndent);
258
- }
259
- }
260
- main: while ((m1 = sniffTestRe.exec(input))) {
261
- if (m1[1] === '--') {
262
- continue;
263
- }
264
- else if (fragment.template) {
265
- fragment.html += m1[0];
266
- }
267
- // If this is a script tag or preformatted block
268
- // we want to retain the intended formatting less
269
- // the indent. Pre-formatting can apply to any element
270
- // by ending the declaration with `:: {`.
271
- if (verbatimIndent != null) {
272
- // inside a script or preformatted block
273
- identRe.lastIndex = 0;
274
- m2 = identRe.exec(m1[0]);
275
- const indent = m2 == null
276
- ? null
277
- : m2[0].length / 2;
278
- if (m2 == null || indent <= verbatimIndent) {
279
- fragment.html += '\n';
280
- applyIndent(indent);
281
- verbatimIndent = null;
282
- verbatimFirst = false;
283
- textIndent = indent;
284
- if (preformattedClose.test(m1[0])) {
261
+ verbatimType = 0;
262
+ verbatimText = '';
263
+ if (preformatClose.has(m1[0].trim()))
285
264
  continue;
286
- }
287
265
  }
288
- else {
289
- const line = m1[0].replace(' '.repeat(verbatimIndent + 1), '');
290
- if (element.tag != null) {
291
- applyIndent(indent);
292
- }
293
- if (verbatimFirst) {
294
- verbatimFirst = false;
295
- }
296
- else {
297
- fragment.html += '\n';
298
- }
299
- if (verbatimSerialize) {
300
- fragment.html += line;
301
- }
302
- else {
303
- fragment.html += escape(line);
304
- }
305
- continue;
306
- }
307
- }
266
+ } // end verbatim
308
267
  if (m1[LINE].trim() === '') {
268
+ // empty lines have no effect from here on
309
269
  continue;
310
270
  }
311
- // The id and lang directives should proceed all other directives and
312
- // fragment declarations.
313
271
  if (doc === undefined) {
272
+ // The meta directives get special treatment.
273
+ let parseBlock = false;
314
274
  const inlineArgs = m1[DIRECTIVE_INLINE_ARGS] ?? '';
315
275
  switch (m1[DIRECTIVE]) {
316
- case 'id': {
276
+ case 'id':
317
277
  const url = inlineArgs.trim();
278
+ parseBlock = true;
318
279
  try {
319
280
  id = id ?? new URL(url, args?.base).toString();
320
281
  }
321
282
  catch (err) {
322
- console.warn(`Invalid URL given to @id directive: ${url} base=${args.base}`);
283
+ console.warn(`Invalid URL given to @id directive: ${url} base=${args?.base}`);
323
284
  }
324
- continue main;
325
- }
326
- case 'lang': {
285
+ break;
286
+ case 'lang':
287
+ parseBlock = true;
327
288
  lang = lang ?? inlineArgs.trim();
328
- continue main;
329
- }
330
- case 'dir': {
289
+ break;
290
+ case 'dir':
291
+ parseBlock = true;
331
292
  dir = dir ?? inlineArgs.trim();
332
- continue main;
333
- }
334
- default: {
335
- const def = directives[m1[DIRECTIVE]];
336
- if (typeof def?.meta === 'function') {
337
- if (Object.keys(def).length > 1) {
338
- throw new Error(`A custom directive performing the meta role cannot be used for other purposes. ` +
339
- `See @${m1[DIRECTIVE]}`);
340
- }
341
- meta[m1[DIRECTIVE]] = def.meta({ inlineArgs });
342
- continue main;
343
- }
344
- }
345
293
  }
346
- doc = new Doc(id, lang, dir, meta, args?.allowAll, args?.allowedAttributes, args?.allowedElements);
347
- if (args?.outputMode === 'meta') {
294
+ const def = directives[m1[DIRECTIVE]];
295
+ if (typeof def?.meta === 'function' || parseBlock) {
296
+ verbatimIndent = indent + 1;
297
+ verbatimType = 4;
298
+ directive = {
299
+ name: m1[DIRECTIVE],
300
+ inlineArgs,
301
+ def,
302
+ };
303
+ continue main;
304
+ }
305
+ if (args?.outputMode === 'head') {
348
306
  return {
349
307
  key,
350
308
  id,
351
309
  lang,
352
310
  dir,
353
- meta: doc.meta,
311
+ meta,
312
+ data,
354
313
  };
355
314
  }
315
+ doc = new Doc(id, lang, dir, meta, args?.allowAll, args?.allowedAttributes, args?.allowedElements);
356
316
  }
357
317
  switch (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) {
358
318
  case '#':
359
- case '##': {
360
- id1.lastIndex = 0;
361
- const indent = (m1[INDENT].length ?? 0) / 2;
362
- if (element.tag != null || textIndent != null) {
363
- applyIndent(indent);
364
- textIndent = null;
365
- }
319
+ case '##':
320
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
366
321
  fragment.id = m1[ID];
367
322
  if (indent === 0) {
368
323
  if (m1[FRAGMENT_TYPE] == '[') {
@@ -383,104 +338,93 @@ function longform(input, args) {
383
338
  element.id = fragment.id;
384
339
  }
385
340
  break;
386
- }
387
- case '@': {
388
- const indent = m1[INDENT].length / 2;
389
- if (element.tag != null || textIndent != null) {
390
- applyIndent(indent);
341
+ case '@':
342
+ if (element.tag !== undefined) {
343
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
391
344
  }
345
+ const inlineArgs = m1[DIRECTIVE_INLINE_ARGS] ?? '';
392
346
  switch (m1[DIRECTIVE]) {
393
- case 'doctype': {
394
- const args = m1[DIRECTIVE_INLINE_ARGS] ?? 'html';
395
- fragment.html += `<!doctype ${args.trim()}>`;
396
- break;
397
- }
398
- case 'xml': {
399
- const args = m1[DIRECTIVE_INLINE_ARGS] ?? 'version="1.0" encoding="UTF-8"';
400
- fragment.html += `<?xml ${args.trim()}?>`;
401
- break;
402
- }
403
- case 'template': {
404
- let indented = false;
405
- fragment.template = indent === 0;
406
- templateLinesRe.lastIndex = sniffTestRe.lastIndex;
407
- while ((m2 = templateLinesRe.exec(input))) {
408
- if (m2[1] == null && !indented && fragment.id == null) {
409
- id1.lastIndex = 0;
410
- m3 = id1.exec(m2[0]);
411
- if (m3 != null)
412
- fragment.id = m3[3];
413
- fragment.html += m2[0];
414
- }
415
- else if (m2[1] == null && indented) {
416
- sniffTestRe.lastIndex = templateLinesRe.lastIndex - m2[0].length;
417
- break;
418
- }
419
- else {
420
- fragment.html += '\n' + m2[0];
421
- if (m2[1] != null)
422
- indented = true;
347
+ case 'template':
348
+ if (indent === 0) {
349
+ let indented = false;
350
+ fragment.template = true;
351
+ templateLinesRe.lastIndex = sniffTestRe.lastIndex;
352
+ while ((m2 = templateLinesRe.exec(input))) {
353
+ if (m2[1] == null && !indented && fragment.id == null) {
354
+ m3 = idRe.exec(m2[0]);
355
+ if (m3 != null)
356
+ fragment.id = m3[3];
357
+ fragment.html += m2[0];
358
+ }
359
+ else if (m2[1] == null && indented) {
360
+ sniffTestRe.lastIndex = templateLinesRe.lastIndex - m2[0].length;
361
+ break;
362
+ }
363
+ else {
364
+ fragment.html += '\n' + m2[0];
365
+ if (m2[1] != null)
366
+ indented = true;
367
+ }
423
368
  }
369
+ [element, fragment] = applyIndent(0, key, element, fragment, doc, parsed, output, args);
424
370
  }
425
- applyIndent(0);
371
+ continue main;
372
+ case 'doctype':
373
+ fragment.html += `<!doctype ${(inlineArgs.trim() || 'html').trim()}>`;
426
374
  break;
427
- }
428
- case 'mount': {
429
- if (m1[DIRECTIVE_INLINE_ARGS] == null) {
430
- throw new Error('Mount points must have a name');
375
+ case 'xml':
376
+ fragment.html += `<?xml ${inlineArgs.trim() || 'version="1.0" encoding="UTF-8"'}?>`;
377
+ break;
378
+ case 'mount':
379
+ if (args?.outputMode !== 'mountable')
380
+ break;
381
+ if (inlineArgs === '') {
382
+ console.warn('Mount points must have a name');
431
383
  }
432
384
  else if (fragment.type !== 'root') {
433
- throw new Error('Mounting is only allowed on a root element');
385
+ console.warn('Mounting is only allowed on a root element');
434
386
  }
435
387
  fragment.mountable = true;
436
- element.mount = m1[DIRECTIVE_INLINE_ARGS].trim();
388
+ element.mount = inlineArgs.trim();
437
389
  break;
438
- }
439
- default: {
440
- const def = directives[m1[DIRECTIVE]];
441
- if (def == null)
442
- break;
443
- if (typeof def.beforeRender === 'function') {
444
- if (element.id != null) {
445
- applyIndent(indent);
446
- }
447
- if (element.beforeRender == null)
448
- element.beforeRender = [];
449
- // TODO: Parse block args
450
- const applied = {
451
- ...def,
452
- inlineArg: m1[DIRECTIVE_INLINE_ARGS],
453
- };
454
- element.beforeRender.push(applied);
455
- }
456
- }
457
390
  }
391
+ const def = directives[m1[DIRECTIVE]];
392
+ // A directive may not be defined but we want to process
393
+ // any block args to keep the output valid. Builtin directives
394
+ // will be ignored unless they require block args.
395
+ verbatimIndent = indent + 1;
396
+ verbatimType = 4;
397
+ directive = {
398
+ name: m1[DIRECTIVE],
399
+ inlineArgs: m1[DIRECTIVE_INLINE_ARGS],
400
+ def,
401
+ };
458
402
  break;
459
- }
460
- case '[':
461
- case '::': {
403
+ case '::':
462
404
  if (m1[ELEMENT] !== undefined) {
463
- const indent = (m1[INDENT]?.length ?? 0) / 2;
464
405
  let preformattedType;
465
406
  let inlineText;
466
407
  if (element.tag !== undefined ||
467
408
  element.indent > indent) {
468
- applyIndent(indent);
409
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
469
410
  }
470
411
  element.indent = indent;
471
- textIndent = null;
472
412
  if (indent === 0 && fragment.id == null) {
473
- if (root != null) ;
413
+ if (root != null) {
414
+ // skip if root is found and this fragment
415
+ // has no id
416
+ skipping = true;
417
+ }
474
418
  else {
475
419
  fragment.type = 'root';
476
420
  root = fragment;
477
421
  }
478
422
  }
479
423
  const parent = element;
480
- outer.lastIndex = 0;
424
+ outerRe.lastIndex = 0;
481
425
  // Looping through chained element declarations
482
426
  // foo#x.y::bar::free::
483
- while ((m2 = outer.exec(m1[LINE]))) {
427
+ while ((m2 = outerRe.exec(m1[LINE]))) {
484
428
  let working;
485
429
  preformattedType = m2[3];
486
430
  inlineText = m2[4];
@@ -495,9 +439,9 @@ function longform(input, args) {
495
439
  working.tag = m2[1];
496
440
  parent.chain.push(working);
497
441
  }
498
- inner.lastIndex = 0;
442
+ innerRe.lastIndex = 0;
499
443
  // Looping through ids, classes and attrs
500
- while ((m3 = inner.exec(m2[2]))) {
444
+ while ((m3 = innerRe.exec(m2[2]))) {
501
445
  if (m3[2] !== undefined) {
502
446
  working.id = m3[2];
503
447
  }
@@ -514,8 +458,8 @@ function longform(input, args) {
514
458
  let value = m3[4] ?? m3[5] ?? m3[6];
515
459
  // attribute directives
516
460
  if (value[0] === '@') {
517
- directiveAttr.lastIndex = 0;
518
- m4 = directiveAttr.exec(value);
461
+ attributeDirectiveRe.lastIndex = 0;
462
+ m4 = attributeDirectiveRe.exec(value);
519
463
  if (m4 != null) {
520
464
  const def = directives[m4[1]];
521
465
  if (def != null && typeof def.attr === 'function') {
@@ -530,20 +474,21 @@ function longform(input, args) {
530
474
  }
531
475
  switch (m3[3]) {
532
476
  case 'id':
533
- if (!working.id) {
477
+ if (!working.id && typeof value === 'string') {
534
478
  working.id = value;
535
479
  }
536
480
  break;
537
481
  case 'class':
538
- if (!working.class) {
482
+ if (!working.class && typeof value === 'string') {
539
483
  working.class = value;
540
484
  }
541
- else {
485
+ else if (typeof value === 'string') {
542
486
  working.class += ' ' + value;
543
487
  }
544
488
  break;
545
489
  default:
546
- working.attrs[m3[3]] = value;
490
+ if (value !== false)
491
+ working.attrs[m3[3]] = value;
547
492
  }
548
493
  }
549
494
  }
@@ -553,29 +498,33 @@ function longform(input, args) {
553
498
  // server specific process.
554
499
  if (element.mount != null) {
555
500
  const id = element.mount;
556
- applyIndent(indent + 1);
501
+ [element, fragment] = applyIndent(indent + 1, key, element, fragment, doc, parsed, output, args);
502
+ if (fragment.mountPoints == null)
503
+ fragment.mountPoints = [];
557
504
  fragment.mountPoints.push({
558
505
  id,
559
506
  part: fragment.html,
560
507
  });
561
508
  fragment.html = '';
562
- applyIndent(indent);
509
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
563
510
  break;
564
511
  }
565
512
  if (preformattedType !== undefined) {
566
- verbatimFirst = true;
567
- verbatimIndent = indent;
568
- verbatimSerialize = preformattedType === '{{';
513
+ if (element.tag !== undefined)
514
+ [element, fragment] = applyIndent(indent + 1, key, element, fragment, doc, parsed, output, args);
515
+ verbatimIndent = indent + 1;
516
+ verbatimType = preformattedType === '{{' ? 3 : 2;
569
517
  }
570
518
  else if (inlineText !== undefined) {
571
519
  element.text = inlineText;
572
520
  }
573
521
  break;
574
522
  }
575
- attribute1.lastIndex = 0;
523
+ case '[':
524
+ attributeRe.lastIndex = 0;
576
525
  m2 = m1[ATTR] !== undefined
577
- ? attribute1.exec(m1[LINE])
578
- : undefined;
526
+ ? attributeRe.exec(m1[LINE])
527
+ : null;
579
528
  if (m2 != null && element.tag != null) {
580
529
  if (m2[2] === 'id') {
581
530
  if (element.id == null) {
@@ -598,69 +547,24 @@ function longform(input, args) {
598
547
  }
599
548
  break;
600
549
  }
601
- }
602
- default: {
603
- m2 = text1.exec(m1[0]);
604
- if (m2 == null) {
605
- break;
606
- }
607
- const indent = m2[1].length / 2;
608
- const tx = m2[2].trim();
609
- if (element.tag != null) {
610
- applyIndent(indent);
611
- fragment.html += tx;
612
- }
613
- else if (fragment.type === 'text' && fragment.html === '') {
614
- fragment.html += tx;
615
- }
616
- else {
617
- fragment.html += ' ' + tx;
618
- }
619
- textIndent = indent;
620
- while ((m2 = refRe.exec(tx))) {
621
- const start = fragment.html.length + m2.index - tx.length;
622
- fragment.refs.push({
623
- id: m2[1],
624
- start,
625
- end: start + m2[0].length,
626
- });
627
- }
628
- break;
629
- }
630
- }
631
- }
632
- applyIndent(0);
633
- const arr = Array.from(parsed.values());
634
- function flatten(fragment) {
635
- if (fragment.refs == null)
636
- fragment.refs = [];
637
- // work backwards so we don't change the html string length
638
- // for the later replacements
639
- for (let j = fragment.refs.length - 1; j >= 0; j--) {
640
- const ref = fragment.refs[j];
641
- if (claimed.has(ref.id) || !parsed.has(ref.id)) {
642
- fragment.html = fragment.html.slice(0, ref.start)
643
- + fragment.html.slice(ref.end);
644
- }
645
- else {
646
- const child = flatten(parsed.get(ref.id));
647
- fragment.html = fragment.html.slice(0, ref.start)
648
- + child.html
649
- + fragment.html.slice(ref.end);
650
- if (child.type === 'embed') {
651
- claimed.add(child.id);
652
- }
653
- }
550
+ default:
551
+ if (element.tag !== undefined)
552
+ [element, fragment] = applyIndent(indent, key, element, fragment, doc, parsed, output, args);
553
+ verbatimText = m1[0].trim();
554
+ verbatimIndent = indent;
555
+ verbatimType = 1;
654
556
  }
655
- fragment.refs = [];
656
- return fragment;
657
557
  }
558
+ applyIndent(0, key, element, fragment, doc, parsed, output, args);
559
+ if (promises.length > 0)
560
+ await Promise.all(promises);
658
561
  if (root?.mountable) {
659
562
  output.mountable = true;
660
563
  output.tail = root.html;
661
- output.mountPoints = root.mountPoints;
564
+ output.mountPoints = root.mountPoints ?? [];
662
565
  return output;
663
566
  }
567
+ const arr = Array.from(parsed.values());
664
568
  for (let i = 0; i < parsed.size + 1; i++) {
665
569
  let fragment;
666
570
  if (i === 0 && root == null) {
@@ -675,33 +579,31 @@ function longform(input, args) {
675
579
  if (fragment.refs == null || fragment.refs.length === 0) {
676
580
  continue;
677
581
  }
678
- flatten(fragment);
582
+ flatten(fragment, claimed, parsed);
679
583
  }
680
584
  if (root?.html != null) {
681
585
  output.root = root.html;
682
586
  output.selector = `[data-${key}-root]`;
683
587
  }
684
- for (let i = 0; i < arr.length; i++) {
588
+ for (let i = 0, l = arr.length, f = arr[i]; i < l; i++) {
685
589
  let selector;
686
- const fragment = arr[i];
687
- if (fragment == null || claimed.has(fragment.id)) {
590
+ if (f == null || claimed.has(f.id)) {
688
591
  continue;
689
592
  }
690
- switch (fragment.type) {
691
- case 'embed': {
593
+ switch (f.type) {
594
+ case 'embed':
692
595
  if (args?.predictable) {
693
- selector = `[id=${fragment.id}]`;
596
+ selector = `[id=${f.id}]`;
694
597
  break;
695
598
  }
696
- }
697
599
  case 'bare':
698
- case 'range': selector = `[data-${key}=${fragment.id}]`;
600
+ case 'range': selector = `[data-${key}=${f.id}]`;
699
601
  }
700
- output.fragments[fragment.id] = {
701
- id: fragment.id,
602
+ output.fragments[f.id] = {
603
+ id: f.id,
702
604
  selector,
703
- type: fragment.type,
704
- html: fragment.html,
605
+ type: f.type,
606
+ html: f.html,
705
607
  };
706
608
  }
707
609
  output.key = key;
@@ -719,9 +621,9 @@ function longform(input, args) {
719
621
  * @param getFragment - A function which returns an already processed fragment's HTML string.
720
622
  * @returns The processed template.
721
623
  */
722
- function processTemplate(template, args, getFragment) {
723
- const lf = template.replace(templateRe, (_match, param, ref) => {
724
- if (ref != null) {
624
+ async function processTemplate(template, args, longformArgs, getFragment) {
625
+ const lf = template.replace(templateRe, (_line, _structured, param, ref) => {
626
+ if (ref !== undefined && typeof getFragment === 'function') {
725
627
  const fragment = getFragment(ref);
726
628
  if (fragment == null)
727
629
  return '';
@@ -729,7 +631,156 @@ function processTemplate(template, args, getFragment) {
729
631
  }
730
632
  return args[param] != null ? escape(args[param].toString()) : '';
731
633
  });
732
- return Object.values(longform(lf).fragments)[0]?.html ?? null;
634
+ return Object.values((await longform(lf, longformArgs)).fragments)[0]?.html ?? null;
635
+ }
636
+ /**
637
+ * Closes any current in progress element definition
638
+ * and creates a new working element.
639
+ */
640
+ function applyIndent(targetIndent, key, element, fragment, doc, parsed, output, args) {
641
+ if (element.tag !== undefined) {
642
+ if (Array.isArray(element.beforeRender)) {
643
+ const el = {
644
+ id: element.id,
645
+ tag: element.tag,
646
+ class: element.class,
647
+ attrs: element.attrs,
648
+ };
649
+ const chain = [];
650
+ for (let i = 0, l = element.chain.length, el = element.chain[i]; i < l; i++) {
651
+ chain.push({
652
+ id: el.id,
653
+ tag: el.tag,
654
+ class: el.class,
655
+ attrs: el.attrs,
656
+ });
657
+ }
658
+ for (let i = 0, l = element.beforeRender.length, def = element.beforeRender[i]; i < l; i++) {
659
+ def.element({
660
+ el,
661
+ chain,
662
+ doc,
663
+ inlineArgs: def.inlineArgs,
664
+ blockArgs: def.blockArgs,
665
+ });
666
+ }
667
+ }
668
+ const root = fragment.type === 'range'
669
+ ? targetIndent < 2
670
+ : fragment.html === '';
671
+ fragment.html += `<${element.tag}`;
672
+ if (element.id !== undefined) {
673
+ fragment.html += ' id="' + element.id + '"';
674
+ }
675
+ if (element.class !== undefined) {
676
+ fragment.html += ' class="' + element.class + '"';
677
+ }
678
+ for (const attr of Object.entries(element.attrs)) {
679
+ if (attr[0] === 'id' || attr[0] === 'class')
680
+ continue;
681
+ if (attr[1] == null) {
682
+ fragment.html += ' ' + attr[0];
683
+ }
684
+ else {
685
+ fragment.html += ` ${attr[0]}="${attr[1]}"`;
686
+ }
687
+ }
688
+ if (root) {
689
+ if (fragment.type === 'root') {
690
+ fragment.html += ` data-${key}-root`;
691
+ }
692
+ else if (fragment.type === 'bare' || fragment.type === 'range') {
693
+ fragment.html += ` data-${key}="${fragment.id}"`;
694
+ }
695
+ else if (fragment.type === 'embed' && !args?.predictable) {
696
+ fragment.html += ` data-${key}="${fragment.id}"`;
697
+ }
698
+ }
699
+ if (element.mount !== undefined) {
700
+ fragment.html += ` data-${key}-mount="${element.mount}"`;
701
+ }
702
+ fragment.html += '>';
703
+ if (Array.isArray(element.chain)) {
704
+ let chained;
705
+ for (let i = 0, l = element.chain.length; i < l; i++) {
706
+ chained = element.chain[i];
707
+ fragment.html += '<' + chained.tag;
708
+ if (chained.id !== undefined) {
709
+ fragment.html += ' id="' + chained.id + '"';
710
+ }
711
+ if (chained.class != undefined) {
712
+ fragment.html += ' class="' + chained.class + '"';
713
+ }
714
+ for (const attr of Object.entries(chained.attrs)) {
715
+ if (attr[1] === undefined) {
716
+ fragment.html += ' ' + attr[0];
717
+ }
718
+ else {
719
+ fragment.html += ` ${attr[0]}="${attr[1]}"`;
720
+ }
721
+ }
722
+ fragment.html += '>';
723
+ }
724
+ }
725
+ if (!voids.has(element.tag) && element.text != null) {
726
+ fragment.html += element.text;
727
+ }
728
+ if (!voids.has(element.tag)) {
729
+ fragment.els.push(element);
730
+ }
731
+ }
732
+ if (targetIndent <= element.indent) {
733
+ element = makeElement(targetIndent);
734
+ while (fragment.els.length !== 0 && (targetIndent == null ||
735
+ fragment.els[fragment.els.length - 1].indent !== targetIndent - 1)) {
736
+ const element = fragment.els.pop();
737
+ if (Array.isArray(element.chain)) {
738
+ for (let i = 0, l = element.chain.length; i < l; i++) {
739
+ fragment.html += `</${element.chain[i].tag}>`;
740
+ }
741
+ }
742
+ fragment.html += `</${element?.tag}>`;
743
+ }
744
+ if (targetIndent === 0) {
745
+ if (fragment.template) {
746
+ output.templates[fragment.id] = fragment.html;
747
+ }
748
+ else if (fragment.type !== 'root') {
749
+ parsed.set(fragment.id, fragment);
750
+ }
751
+ fragment = makeFragment();
752
+ }
753
+ }
754
+ else {
755
+ element = makeElement(targetIndent);
756
+ }
757
+ return [element, fragment];
758
+ }
759
+ function flatten(fragment, claimed, parsed, locals = new Set()) {
760
+ let r;
761
+ if (Array.isArray(fragment.refs)) {
762
+ for (let i = fragment.refs.length - 1; i > -1; i--) {
763
+ r = fragment.refs[i];
764
+ if (locals.has(r.id) || claimed.has(r.id) || !parsed.has(r.id)) {
765
+ // cannot use fragment here. Clear the reference marker
766
+ fragment.html = fragment.html.slice(0, r.start)
767
+ + fragment.html.slice(r.end);
768
+ }
769
+ else {
770
+ locals.add(fragment.id);
771
+ const child = flatten(parsed.get(r.id), claimed, parsed, locals);
772
+ fragment.html = fragment.html.slice(0, r.start)
773
+ + child.html
774
+ + fragment.html.slice(r.end);
775
+ if (child.type === 'embed') {
776
+ claimed.add(child.id);
777
+ }
778
+ }
779
+ }
780
+ }
781
+ // don't re-process the fragment
782
+ fragment.refs = undefined;
783
+ return fragment;
733
784
  }
734
785
 
735
786
  export { longform, processTemplate };