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