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