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