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