@longform/longform 0.0.5 → 0.0.7

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/lib/longform.ts CHANGED
@@ -1,17 +1,40 @@
1
- import type { FragmentType, ParsedResult, WorkingElement, WorkingFragment, Fragment } from "./types.ts";
2
-
3
- const sniffTestRe = /^(?:(?:(--).*)|(?: *(@|#).*)|(?: *[\w\-]+(?::[\w\-]+)?(?:[#.[][^\n]+)?(::).*)|(?: +([\["]).*)|(\ \ .*))$/gmi
4
- , element1 = /((?:\ \ )+)? ?([\w\-]+(?::[\w\-]+)?)([#\.\[][^\n]*)?::(?: ({{?|[^\n]+))?/gmi
5
- , directive1 = /((?:\ \ )+)? ?@([\w][\w\-]+)(?::: ?([^\n]+)?)?/gmi
1
+ import type { FragmentType, ParsedResult, WorkingElement, WorkingFragment } from "./types.ts";
2
+
3
+ const LINE = 0
4
+ , INDENT = 1
5
+ , DIRECTIVE_KEY = 2
6
+ , DIRECTIVE = 3
7
+ , DIRECTIVE_INLINE_ARGS = 4
8
+ , ID_TYPE = 5
9
+ , ID = 6
10
+ , FRAGMENT_TYPE = 7
11
+ , ELEMENT = 8
12
+ , ATTR = 9
13
+ ;
14
+
15
+ 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
16
+
17
+ // captures a single element definition which could be in a chain.
18
+ // id, class and attributes are matched as a single block for a later loop
19
+ // if in chained situation the regexp will need to be looped over for each element
20
+ // we know the element is the last if the final capture group has a positive match
21
+ // or if the line is consumed by the regexp.
22
+ //, outer = /([a-z][\w\-]*(?::[a-z][\w\-]*)?)([^:]+)*::(?: (?:({{?)|(.*)))?/gi
23
+ , outer = /([a-z][\w\-]*(?::[a-z][\w\-]*)?)((?:(?:[^:])|(?::(?!:)))*)::(?: (?:({{?)|(.*)))?/gi
24
+ //, outer = /([a-z][\w\-]*(?::[a-z][\w\-]*)?)(.+?(?=::))?(?: (?:({{?)|(.*)))?/gi
25
+
26
+ // captures each id, class and attribute declaration in an element
27
+ , inner = /(?:\.([a-z][\w\-]+)|#([a-z][\w\-]+)|\[([a-z][a-z\-]+(?::[a-z][a-z|\-]*)?)(?:=(?:"([^"]+)"|'([^']+)'|([^\]]+)))?\])/gi
6
28
  , attribute1 = /((?:\ \ )+)\[(\w[\w-]*(?::\w[\w-]*)?)(?:=([^\n]+))?\]/
7
29
  , preformattedClose = /[ \t]*}}?[ \t]*/
8
30
  , id1 = /((?:\ \ )+)?#(#)?([\w\-]+)(?: ([\["]))?/gmi
9
31
  , idnt1 = /^(\ \ )+/
10
32
  , text1 = /^((?:\ \ )+)([^ \n][^\n]*)$/i
11
- , paramsRe = /(?:(#|\.)([^#.\[\n]+)|(?:\[(\w[\w\-]*(?::\w[\w\-]*)?)(?:=([^\n\]]+))?\]))/g
12
33
  , refRe = /#\[([\w\-]+)\]/g
13
34
  , escapeRe = /([&<>"'#\[\]{}])/g
14
35
  , templateLinesRe = /^(\ \ )?([^\n]+)$/gmi
36
+
37
+ // TODO: Benchmark v Array.includes()
15
38
  , voids = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wrb']);
16
39
 
17
40
  let m1: RegExpExecArray | null
@@ -42,6 +65,7 @@ function makeElement(indent: number = 0): WorkingElement {
42
65
  indent,
43
66
  html: '',
44
67
  attrs: {},
68
+ chain: undefined,
45
69
  };
46
70
  }
47
71
 
@@ -83,7 +107,6 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
83
107
  output.fragments = Object.create(null);
84
108
  output.templates = Object.create(null);
85
109
 
86
-
87
110
  /**
88
111
  * Closes any current in progress element definition
89
112
  * and creates a new working element.
@@ -105,15 +128,15 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
105
128
  }
106
129
  }
107
130
 
108
- if (element.mount != null) {
131
+ if (element.mount !== undefined) {
109
132
  fragment.html += ` data-lf-mount="${element.mount}"`;
110
133
  }
111
134
 
112
- if (element.id != null) {
135
+ if (element.id !== undefined) {
113
136
  fragment.html += ' id="' + element.id + '"';
114
137
  }
115
138
 
116
- if (element.class != null) {
139
+ if (element.class !== undefined) {
117
140
  fragment.html += ' class="' + element.class + '"';
118
141
  }
119
142
 
@@ -127,6 +150,34 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
127
150
 
128
151
  fragment.html += '>';
129
152
 
153
+ if (Array.isArray(element.chain)) {
154
+ let chained: WorkingElement;
155
+
156
+ for (let i = 0, l = element.chain.length; i < l; i++) {
157
+ chained = element.chain[i];
158
+
159
+ fragment.html += '<' + chained.tag;
160
+
161
+ if (chained.id !== undefined) {
162
+ fragment.html += ' id="' + chained.id + '"';
163
+ }
164
+
165
+ if (chained.class != undefined) {
166
+ fragment.html += ' class="' + chained.class + '"';
167
+ }
168
+
169
+ for (const attr of Object.entries(chained.attrs)) {
170
+ if (attr[1] === undefined) {
171
+ fragment.html += ' ' + attr[0]
172
+ } else {
173
+ fragment.html += ` ${attr[0]}="${attr[1]}"`;
174
+ }
175
+ }
176
+
177
+ fragment.html += '>';
178
+ }
179
+ }
180
+
130
181
  if (!voids.has(element.tag as string) && element.text != null) {
131
182
  fragment.html += element.text;
132
183
  }
@@ -149,6 +200,12 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
149
200
  ) {
150
201
  const element = fragment.els.pop();
151
202
 
203
+ if (Array.isArray(element.chain)) {
204
+ for (let i = 0, l = element.chain.length; i < l; i++) {
205
+ fragment.html += `</${element.chain[i].tag}>`;
206
+ }
207
+ }
208
+
152
209
  fragment.html += `</${element?.tag}>`;
153
210
  }
154
211
 
@@ -215,88 +272,130 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
215
272
  }
216
273
 
217
274
  if (verbatimSerialize) {
218
- fragment.html += escape(line);
219
- } else {
220
275
  fragment.html += line;
276
+ } else {
277
+ fragment.html += escape(line);
221
278
  }
222
279
 
223
280
  continue;
224
281
  }
225
282
  }
226
283
 
227
- if (m1[0].trim() === '') {
284
+ if (m1[LINE].trim() === '') {
228
285
  continue;
229
286
  }
230
287
 
231
- switch (m1[2] ?? m1[3] ?? m1[4]) {
232
- // deno-lint-ignore no-fallthrough
233
- case '#': {
288
+ switch (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) {
289
+ case '#':
290
+ case '##': {
234
291
  id1.lastIndex = 0;
235
- m2 = id1.exec(m1[0]);
236
292
 
237
- if (m2 != null) {
238
- const indent = (m2[1]?.length ?? 0) / 2;
293
+ const indent = (m1[INDENT].length ?? 0) / 2;
239
294
 
240
- if (element.tag != null || textIndent != null) {
241
- applyIndent(indent);
242
- textIndent = null;
295
+ if (element.tag != null || textIndent != null) {
296
+ applyIndent(indent);
297
+ textIndent = null;
298
+ }
299
+
300
+ fragment.id = m1[ID];
301
+
302
+ if (indent === 0) {
303
+ if (m1[FRAGMENT_TYPE] == '[') {
304
+ fragment.type = 'range';
305
+ } else if (m1[FRAGMENT_TYPE] === '"') {
306
+ fragment.type = 'text';
307
+ } else if (m1[ID_TYPE] === '##') {
308
+ fragment.type = 'bare';
309
+ } else {
310
+ fragment.type = 'embed';
311
+ element.id = fragment.id;
243
312
  }
313
+ } else {
314
+ element.id = fragment.id;
315
+ }
244
316
 
245
- debug(indent, 'id', m2[2], m2[3], m2[4]);
317
+ break;
318
+ }
319
+ case '@': {
320
+ const indent = m1[INDENT].length / 2;
246
321
 
247
- fragment.id = m2[3];
322
+ if (element.tag != null || textIndent != null) {
323
+ applyIndent(indent);
324
+ }
248
325
 
249
- if (indent === 0) {
250
- if (m2[4] == '[') {
251
- fragment.type = 'range';
252
- } else if (m2[4] === '"') {
253
- fragment.type = 'text';
254
- } else if (m2[2] != null) {
255
- fragment.type = 'bare';
256
- } else {
257
- fragment.type = 'embed';
258
- element.id = fragment.id;
326
+ switch (m1[DIRECTIVE]) {
327
+ case 'doctype': {
328
+ const args = m1[DIRECTIVE_INLINE_ARGS] ?? 'html';
329
+ fragment.html += `<!doctype ${args.trim()}>`;
330
+ break;
331
+ }
332
+ case 'xml': {
333
+ const args = m1[DIRECTIVE_INLINE_ARGS] ?? 'version="1.0" encoding="UTF-8"';
334
+ fragment.html += `<?xml ${args.trim()}?>`;
335
+ break;
336
+ }
337
+ case 'template': {
338
+ let indented = false;
339
+ fragment.template = indent === 0;
340
+
341
+ templateLinesRe.lastIndex = sniffTestRe.lastIndex;
342
+ while ((m2 = templateLinesRe.exec(doc))) {
343
+ if (m2[1] == null && !indented && fragment.id == null) {
344
+ id1.lastIndex = 0;
345
+ m3 = id1.exec(m2[0]);
346
+
347
+ if (m3 != null) fragment.id = m3[3];
348
+
349
+ fragment.html += m2[0];
350
+ } else if (m2[1] == null && indented) {
351
+ sniffTestRe.lastIndex = templateLinesRe.lastIndex - m2[0].length;
352
+ break;
353
+ } else {
354
+ fragment.html += '\n' + m2[0];
355
+ if (m2[1] != null) indented = true;
356
+ }
259
357
  }
358
+
359
+ applyIndent(0);
360
+ break;
260
361
  }
362
+ case 'mount': {
363
+ if (m1[DIRECTIVE_INLINE_ARGS] == null) {
364
+ throw new Error('Mount points must have a name');
365
+ } else if (fragment.type !== 'root') {
366
+ throw new Error('Mounting is only allowed on a root element');
367
+ }
261
368
 
262
- break;
369
+ fragment.mountable = true;
370
+ element.mount = m1[DIRECTIVE_INLINE_ARGS].trim();
371
+ break;
372
+ }
263
373
  }
374
+
375
+ break;
264
376
  }
265
- case '@':
266
377
  case '[':
267
- // deno-lint-ignore no-fallthrough
268
378
  case '::': {
269
- element1.lastIndex = 0;
270
- // fall through if m1[3] is a # or @
271
- m2 = m1[2] ?? m1[4] != null
272
- ? null
273
- : element1.exec(m1[0]);
274
-
275
- // if null then invalid element selector
276
- // allow the default text case to handle
277
- if (m2 != null) {
278
- const indent = (m2[1]?.length ?? 0) / 2
279
- , tg = m2[2]
280
- , ar = m2[3]
281
- , pr = m2[4] === '{' || m2[4] === '{{'
282
- const tx = pr ? null : m2[4]
283
-
284
- debug(indent, 'e', tg, pr, tx);
379
+ if (m1[ELEMENT] !== undefined) {
380
+ const indent = (m1[INDENT]?.length ?? 0) / 2;
381
+ let preformattedType: string | undefined;
382
+ let inlineText: string | undefined;
285
383
 
286
384
  if (
287
- element.tag != null ||
385
+ element.tag !== undefined ||
288
386
  element.indent > indent
289
387
  ) {
290
388
  applyIndent(indent);
291
389
  }
292
390
 
293
391
  element.indent = indent;
294
- element.tag = tg;
295
392
 
296
393
  textIndent = null;
297
394
 
298
395
  if (indent === 0 && fragment.id == null) {
299
396
  if (root != null) {
397
+ // skip if root is found and this fragment
398
+ // has no id
300
399
  skipping = true;
301
400
  } else {
302
401
  fragment.type = 'root';
@@ -304,30 +403,58 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
304
403
  }
305
404
  }
306
405
 
307
- if (ar != null) {
308
- debug(indent, 'a', ar);
309
- while ((m2 = paramsRe.exec(ar))) {
310
- if (m2[1] === '#') {
311
- element.id = m2[2];
312
- } else if (m2[1] === '.') {
313
- if (element.class == null) {
314
- element.class = m2[2];
406
+ const parent = element;
407
+ outer.lastIndex = 0;
408
+
409
+
410
+ // Looping through chained element declarations
411
+ // foo#x.y::bar::free::
412
+ while ((m2 = outer.exec(m1[LINE]))) {
413
+ let working: WorkingElement;
414
+
415
+ preformattedType = m2[3];
416
+ inlineText = m2[4];
417
+
418
+ if (element.tag === undefined) {
419
+ element.tag = m2[1];
420
+ working = element;
421
+ } else {
422
+ if (parent.chain === undefined) parent.chain = [];
423
+ working = makeElement(indent);
424
+ working.tag = m2[1];
425
+ parent.chain.push(working);
426
+ }
427
+
428
+ inner.lastIndex = 0;
429
+ // Looping through ids, classes and attrs
430
+ while((m3 = inner.exec(m2[2]))) {
431
+ if (m3[2] !== undefined) {
432
+ working.id = m3[2];
433
+ } else if (m3[1] !== undefined) {
434
+ if (working.class == null) {
435
+ working.class = m3[1];
315
436
  } else {
316
- element.class += ' ' + m2[2];
437
+ working.class += ' ' + m3[1];
317
438
  }
318
439
  } else {
319
- if (m2[3] === 'id') {
320
- if (element.id == null) {
321
- element.id = m2[4];
322
- }
323
- } else if (m2[3] === 'class') {
324
- if (element.class == null) {
325
- element.class = m2[4]
326
- } else {
327
- element.class += ' ' + m2[4]
328
- }
329
- } else {
330
- element.attrs[m2[3]] = m2[4];
440
+ // TODO: Preserve quoting style around attribute values
441
+ let value = m3[4] ?? m3[5] ?? m3[6];
442
+
443
+ switch (m3[3]) {
444
+ case 'id':
445
+ if (!working.id) {
446
+ working.id = value;
447
+ }
448
+ break;
449
+ case 'class':
450
+ if (!working.class) {
451
+ working.class = value;
452
+ } else {
453
+ working.class += ' ' + value;
454
+ }
455
+ break;
456
+ default:
457
+ working.attrs[m3[3]] = value;
331
458
  }
332
459
  }
333
460
  }
@@ -347,22 +474,22 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
347
474
  applyIndent(indent);
348
475
  break;
349
476
  }
350
-
351
- if (!pr && tx != null) {
352
- element.text = tx;
353
- } else if (pr) {
477
+
478
+ if (preformattedType !== undefined) {
354
479
  verbatimFirst = true;
355
480
  verbatimIndent = indent;
356
- verbatimSerialize = m2[4] === '{';
481
+ verbatimSerialize = preformattedType === '{{';
482
+ } else if (inlineText !== undefined) {
483
+ element.text = inlineText;
357
484
  }
358
485
 
359
486
  break;
360
487
  }
361
488
 
362
489
  attribute1.lastIndex = 0;
363
- m2 = m1[2] != null
364
- ? null
365
- : attribute1.exec(m1[0]);
490
+ m2 = m1[ATTR] !== undefined
491
+ ? attribute1.exec(m1[LINE])
492
+ : undefined;
366
493
 
367
494
  if (m2 != null && element.tag != null) {
368
495
  debug('a', m2[2], m2[3]);
@@ -385,71 +512,6 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
385
512
 
386
513
  break;
387
514
  }
388
-
389
- directive1.lastIndex = 0;
390
- m2 = m1[3] != null
391
- ? null
392
- : directive1.exec(m1[0]);
393
-
394
- if (m2 != null) {
395
- const indent = (m2[1]?.length ?? 0) / 2;
396
-
397
- if (element.tag != null || textIndent != null) {
398
- applyIndent(indent);
399
- }
400
-
401
- debug(indent, 'd', m2[2], m2[3]);
402
-
403
- switch (m2[2]) {
404
- case 'doctype': {
405
- fragment.html += `<!doctype ${m2[3] ?? 'html'}>`;
406
- break;
407
- }
408
- case 'xml': {
409
- fragment.html += `<?xml ${m2[3] ?? 'version="1.0" encoding="UTF-8"'}?>`;
410
- break;
411
- }
412
- case 'template': {
413
- let indented = false;
414
- fragment.template = indent === 0;
415
-
416
- templateLinesRe.lastIndex = sniffTestRe.lastIndex;
417
- while ((m2 = templateLinesRe.exec(doc))) {
418
- if (m2[1] == null && !indented && fragment.id == null) {
419
- id1.lastIndex = 0;
420
- m3 = id1.exec(m2[0]);
421
-
422
- if (m3 != null) fragment.id = m3[3];
423
-
424
- fragment.html += m2[0];
425
- } else if (m2[1] == null && indented) {
426
- sniffTestRe.lastIndex = templateLinesRe.lastIndex - m2[0].length;
427
- break;
428
- } else {
429
- fragment.html += '\n' + m2[0];
430
- if (m2[1] != null) indented = true;
431
- }
432
- }
433
-
434
- applyIndent(0);
435
- break;
436
- }
437
- case 'mount': {
438
- if (m2[3] == null) {
439
- throw new Error('Mount points must have a name');
440
- } else if (fragment.type !== 'root') {
441
- throw new Error('Mounting is only allowed on a root element');
442
- }
443
-
444
- fragment.mountable = true;
445
- element.mount = m2[3].trim();
446
- break;
447
- }
448
- }
449
-
450
- break;
451
- }
452
-
453
515
  }
454
516
  default: {
455
517
  m2 = text1.exec(m1[0]) as RegExpExecArray;
package/lib/types.ts CHANGED
@@ -9,6 +9,7 @@ export type WorkingElement = {
9
9
  text?: string;
10
10
  html: string;
11
11
  mount?: string;
12
+ chain: WorkingElement[];
12
13
  };
13
14
 
14
15
  export type WorkingFragmentType =
@@ -59,8 +60,8 @@ export type MountPoint = {
59
60
 
60
61
  export type ParsedResult = {
61
62
  mountable?: boolean;
62
- root: string | null;
63
- selector: string | null;
63
+ root?: string;
64
+ selector?: string;
64
65
  mountPoints: MountPoint[];
65
66
  tail?: string;
66
67
  fragments: Record<string, Fragment>;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": "Matthew Quinn",
5
5
  "homepage": "https://github.com/occultist-dev/longform",
6
6
  "license": "MIT",
7
- "version": "0.0.5",
7
+ "version": "0.0.7",
8
8
  "type": "module",
9
9
  "main": "dist/longform.js",
10
10
  "types": "dist/mod.d.ts",
@@ -33,6 +33,7 @@
33
33
  "devDependencies": {
34
34
  "@rollup/plugin-typescript": "^12.3.0",
35
35
  "@types/commonmark": "^0.27.10",
36
+ "@types/deno": "^2.5.0",
36
37
  "@types/jsdom": "^27.0.0",
37
38
  "@types/node": "^24.8.1",
38
39
  "commonmark": "^0.31.2",
@@ -54,6 +55,7 @@
54
55
  ],
55
56
  "scripts": {
56
57
  "build": "node build.ts",
58
+ "bench": "deno bench --no-check --allow-read ./bench/longform.bench.ts",
57
59
  "test": "node --test lib/longform.test.ts",
58
60
  "serve": "node ./serve.ts"
59
61
  }