@longform/longform 0.0.7 → 0.0.9

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,4 +1,4 @@
1
- import type { FragmentType, ParsedResult, WorkingElement, WorkingFragment } from "./types.ts";
1
+ import type { AppliedDirective, DirectiveDef, FragmentType, LongformArgs, ParsedResult, SerializationElement, SerializerConfig, SimplifiedElement, WorkingElement, WorkingFragment } from "./types.ts";
2
2
 
3
3
  const LINE = 0
4
4
  , INDENT = 1
@@ -19,22 +19,19 @@ const sniffTestRe = /^((?: )*)(?:(@)([a-z][a-z\-]*(?::[a-z][a-z\-]*)?)(?:(?:::
19
19
  // if in chained situation the regexp will need to be looped over for each element
20
20
  // we know the element is the last if the final capture group has a positive match
21
21
  // or if the line is consumed by the regexp.
22
- //, outer = /([a-z][\w\-]*(?::[a-z][\w\-]*)?)([^:]+)*::(?: (?:({{?)|(.*)))?/gi
23
22
  , outer = /([a-z][\w\-]*(?::[a-z][\w\-]*)?)((?:(?:[^:])|(?::(?!:)))*)::(?: (?:({{?)|(.*)))?/gi
24
- //, outer = /([a-z][\w\-]*(?::[a-z][\w\-]*)?)(.+?(?=::))?(?: (?:({{?)|(.*)))?/gi
25
23
 
26
24
  // captures each id, class and attribute declaration in an element
27
25
  , inner = /(?:\.([a-z][\w\-]+)|#([a-z][\w\-]+)|\[([a-z][a-z\-]+(?::[a-z][a-z|\-]*)?)(?:=(?:"([^"]+)"|'([^']+)'|([^\]]+)))?\])/gi
28
26
  , attribute1 = /((?:\ \ )+)\[(\w[\w-]*(?::\w[\w-]*)?)(?:=([^\n]+))?\]/
29
27
  , preformattedClose = /[ \t]*}}?[ \t]*/
30
28
  , id1 = /((?:\ \ )+)?#(#)?([\w\-]+)(?: ([\["]))?/gmi
31
- , idnt1 = /^(\ \ )+/
29
+ , identRe = /^(\ \ )+/
32
30
  , text1 = /^((?:\ \ )+)([^ \n][^\n]*)$/i
33
31
  , refRe = /#\[([\w\-]+)\]/g
34
32
  , escapeRe = /([&<>"'#\[\]{}])/g
35
33
  , templateLinesRe = /^(\ \ )?([^\n]+)$/gmi
36
-
37
- // TODO: Benchmark v Array.includes()
34
+ , templateRe = /\#\{(@)?([a-z][\w\-]*(?::[a-z][\w\-]*))}/g
38
35
  , voids = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wrb']);
39
36
 
40
37
  let m1: RegExpExecArray | null
@@ -63,9 +60,17 @@ function escape(value: string): string {
63
60
  function makeElement(indent: number = 0): WorkingElement {
64
61
  return {
65
62
  indent,
66
- html: '',
63
+ key: undefined,
64
+ id: undefined,
65
+ tag: undefined,
66
+ class: undefined,
67
+ text: undefined,
67
68
  attrs: {},
69
+ html: '',
70
+ mount: undefined,
71
+ serializerConfig: undefined,
68
72
  chain: undefined,
73
+ beforeRender: undefined,
69
74
  };
70
75
  }
71
76
 
@@ -81,14 +86,70 @@ function makeFragment(type: FragmentType = 'bare'): WorkingFragment {
81
86
  };
82
87
  }
83
88
 
89
+ class Doc {
90
+ id?: string = undefined;
91
+ lang?: string = undefined;
92
+ meta: Record<string, unknown> = {};
93
+ #serializerConfig: SerializerConfig;
94
+
95
+ constructor(
96
+ id?: string | undefined,
97
+ lang?: string | undefined,
98
+ allowAll?: boolean | undefined,
99
+ allowedAttributes?: string[] | undefined,
100
+ allowedElements?: Array<string | SerializationElement>,
101
+ ) {
102
+ this.id = id;
103
+ this.lang = lang;
104
+ this.#serializerConfig = {
105
+ allowAll: allowAll ?? false,
106
+ allowedAttributes: allowedAttributes ?? [],
107
+ allowedElements: {},
108
+ };
109
+
110
+ if (allowedElements != null) {
111
+ for (let i = 0, l = allowedElements.length, el = allowedElements[i]; i < l; i++) {
112
+ if (typeof el === 'string') {
113
+ this.#serializerConfig.allowedElements[el] = {
114
+ tag: el,
115
+ attrs: [],
116
+ };
117
+ } else {
118
+ this.#serializerConfig.allowedElements[el.tag] = el;
119
+ }
120
+ }
121
+ }
122
+
123
+ Object.freeze(this);
124
+ }
125
+
126
+ allowAll() {
127
+
128
+ }
129
+
130
+ allowAttributes() {
131
+
132
+ }
133
+
134
+ allowElements() {
135
+
136
+ }
137
+ }
138
+
139
+ const directiveValidator = /^[a-z][a-z\-]*\:[a-z][a-z\-]*$/gi;
140
+
141
+
84
142
  /**
85
143
  * Parses a longform document into a object containing the root and fragments
86
144
  * in the output format.
87
145
  *
88
- * @param {string} doc - The longform document to parse.
89
- * @returns {ParsedResult}
146
+ * @param input - The Longform document to parse.
147
+ * @param args - Arguments for the Longform parser.
90
148
  */
91
- export function longform(doc: string, debug: (...d: unknown[]) => void = () => {}): ParsedResult {
149
+ export function longform(
150
+ input: string,
151
+ args?: LongformArgs,
152
+ ): ParsedResult {
92
153
  let skipping: boolean = false
93
154
  , textIndent: number | null = null
94
155
  , verbatimSerialize: boolean = true
@@ -98,20 +159,68 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
98
159
  , fragment: WorkingFragment = makeFragment()
99
160
  // the root fragment
100
161
  , root: WorkingFragment | null = null
162
+ , id: string | undefined
163
+ , lang: string | undefined
164
+ , doc: Doc | undefined
101
165
  // ids of claimed fragments
102
166
  const claimed: Set<string> = new Set()
103
167
  // parsed fragments
104
168
  , parsed: Map<string, WorkingFragment> = new Map()
105
- , output: ParsedResult = Object.create(null);
169
+ , output: ParsedResult = Object.create(null)
170
+ , directives: Record<string, DirectiveDef> = {}
171
+
106
172
 
107
173
  output.fragments = Object.create(null);
108
174
  output.templates = Object.create(null);
175
+
176
+ if (args?.directives != null) {
177
+ const entries = Object.entries(args.directives);
178
+
179
+ for (let i = 0, l = entries.length; i < l; i++) {
180
+ if (!directiveValidator.test(entries[i][0])) {
181
+ console.warn(`Invalid custom directive name '${entries[i][0]}'`);
182
+
183
+ continue;
184
+ }
109
185
 
186
+ directives[entries[i][0]] = entries[i][1];
187
+ }
188
+ }
189
+
110
190
  /**
111
191
  * Closes any current in progress element definition
112
192
  * and creates a new working element.
113
193
  */
114
194
  function applyIndent(targetIndent: number) {
195
+ if (Array.isArray(element.beforeRender)) {
196
+ const el: SimplifiedElement = {
197
+ id: element.id,
198
+ tag: element.tag,
199
+ class: element.class,
200
+ attrs: element.attrs,
201
+ };
202
+ const chain: SimplifiedElement[] = [];
203
+
204
+ for (let i = 0, l = element.chain.length, el = element.chain[i]; i < l; i++) {
205
+ chain.push({
206
+ id: el.id,
207
+ tag: el.tag,
208
+ class: el.class,
209
+ attrs: el.attrs,
210
+ });
211
+ }
212
+
213
+ for (let i = 0, l = element.beforeRender.length, def = element.beforeRender[i]; i < l; i++) {
214
+ def.beforeRender({
215
+ el,
216
+ chain,
217
+ doc,
218
+ inlineArg: def.inlineArg,
219
+ blockArg: def.blockArg,
220
+ });
221
+ }
222
+ }
223
+
115
224
  if (element.tag != null) {
116
225
  const root = fragment.type === 'range'
117
226
  ? targetIndent < 2
@@ -210,7 +319,6 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
210
319
  }
211
320
 
212
321
  if (targetIndent === 0) {
213
- debug(0, '<', fragment.type, fragment.id);
214
322
  if (fragment.template) {
215
323
  output.templates[fragment.id] = fragment.html;
216
324
  } else if (fragment.type === 'root') {
@@ -226,7 +334,7 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
226
334
  }
227
335
  }
228
336
 
229
- while ((m1 = sniffTestRe.exec(doc))) {
337
+ main: while ((m1 = sniffTestRe.exec(input))) {
230
338
  if (m1[1] === '--') {
231
339
  continue;
232
340
  } else if (fragment.template) {
@@ -239,15 +347,14 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
239
347
  // by ending the declaration with `:: {`.
240
348
  if (verbatimIndent != null) {
241
349
  // inside a script or preformatted block
242
- idnt1.lastIndex = 0;
243
- m2 = idnt1.exec(m1[0]);
350
+ identRe.lastIndex = 0;
351
+ m2 = identRe.exec(m1[0]);
244
352
  const indent = m2 == null
245
353
  ? null
246
354
  : m2[0].length / 2;
247
355
 
248
356
  if (m2 == null || indent as number <= verbatimIndent) {
249
357
  fragment.html += '\n';
250
- debug(indent, '}', m2?.[0]);
251
358
 
252
359
  applyIndent(indent);
253
360
  verbatimIndent = null;
@@ -259,7 +366,6 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
259
366
  }
260
367
  } else {
261
368
  const line = m1[0].replace(' '.repeat(verbatimIndent + 1), '');
262
- debug(indent, '{', line);
263
369
 
264
370
  if (element.tag != null) {
265
371
  applyIndent(indent as number);
@@ -285,6 +391,26 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
285
391
  continue;
286
392
  }
287
393
 
394
+ // The id and lang directives should proceed all other directives and
395
+ // fragment declarations.
396
+ if (doc === undefined) {
397
+ const inlineArgs = m1[DIRECTIVE_INLINE_ARGS] ?? '';
398
+
399
+ switch (m1[DIRECTIVE]) {
400
+ case 'id': {
401
+ // TODO: Add id validation
402
+ id = inlineArgs.trim();
403
+ continue main;
404
+ }
405
+ case 'lang': {
406
+ lang = inlineArgs.trim();
407
+ continue main;
408
+ }
409
+ }
410
+
411
+ doc = new Doc(id, lang, args?.allowAll, args?.allowedAttributes, args?.allowedElements);
412
+ }
413
+
288
414
  switch (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) {
289
415
  case '#':
290
416
  case '##': {
@@ -339,7 +465,7 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
339
465
  fragment.template = indent === 0;
340
466
 
341
467
  templateLinesRe.lastIndex = sniffTestRe.lastIndex;
342
- while ((m2 = templateLinesRe.exec(doc))) {
468
+ while ((m2 = templateLinesRe.exec(input))) {
343
469
  if (m2[1] == null && !indented && fragment.id == null) {
344
470
  id1.lastIndex = 0;
345
471
  m3 = id1.exec(m2[0]);
@@ -370,6 +496,27 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
370
496
  element.mount = m1[DIRECTIVE_INLINE_ARGS].trim();
371
497
  break;
372
498
  }
499
+ default: {
500
+ const def = directives[m1[DIRECTIVE]];
501
+
502
+ if (def == null) break;
503
+
504
+ if (typeof def.beforeRender === 'function') {
505
+ if (element.id != null) {
506
+ applyIndent(indent);
507
+ }
508
+
509
+ if (element.beforeRender == null) element.beforeRender = [];
510
+
511
+ // TODO: Parse block args
512
+ const applied: AppliedDirective = {
513
+ ...def,
514
+ inlineArg: m1[DIRECTIVE_INLINE_ARGS],
515
+ };
516
+
517
+ element.beforeRender.push(applied);
518
+ }
519
+ }
373
520
  }
374
521
 
375
522
  break;
@@ -492,8 +639,6 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
492
639
  : undefined;
493
640
 
494
641
  if (m2 != null && element.tag != null) {
495
- debug('a', m2[2], m2[3]);
496
-
497
642
  if (m2[2] === 'id') {
498
643
  if (element.id == null) {
499
644
  element.id = m2[3].trim();
@@ -522,8 +667,6 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
522
667
  const indent = m2[1].length / 2;
523
668
  const tx = m2[2].trim();
524
669
 
525
- debug(indent, 't', m2[2]);
526
-
527
670
  if (element.tag != null) {
528
671
  applyIndent(indent);
529
672
 
@@ -637,11 +780,13 @@ export function longform(doc: string, debug: (...d: unknown[]) => void = () => {
637
780
  };
638
781
  }
639
782
 
783
+ output.id = doc?.id;
784
+ output.lang = doc?.lang;
785
+
640
786
  return output;
641
787
  }
642
788
 
643
789
 
644
- const templateRe = /(?:#{([\w][\w\-_]*)})|(?:#\[([\w][\w\-_]+)\])/g;
645
790
 
646
791
  /**
647
792
  * Processes a client side Longform template to HTML fragment string.
package/lib/types.ts CHANGED
@@ -1,4 +1,16 @@
1
1
 
2
+
3
+ export type SerializationElement = {
4
+ tag: string;
5
+ attrs: string[];
6
+ };
7
+
8
+ export type SerializerConfig = {
9
+ allowAll: boolean;
10
+ allowedAttributes: string[];
11
+ allowedElements: Record<string, SerializationElement>;
12
+ };
13
+
2
14
  export type WorkingElement = {
3
15
  indent: number;
4
16
  key?: string;
@@ -9,7 +21,9 @@ export type WorkingElement = {
9
21
  text?: string;
10
22
  html: string;
11
23
  mount?: string;
24
+ serializerConfig?: SerializerConfig;
12
25
  chain: WorkingElement[];
26
+ beforeRender?: AppliedDirective[];
13
27
  };
14
28
 
15
29
  export type WorkingFragmentType =
@@ -43,7 +57,7 @@ export type WorkingFragment = {
43
57
  html: string;
44
58
  refs: FragmentRef[];
45
59
  els: WorkingElement[];
46
- mountPoints: MountPoint[];
60
+ mountPoints?: MountPoint[];
47
61
  };
48
62
 
49
63
  export type Fragment = {
@@ -59,6 +73,8 @@ export type MountPoint = {
59
73
  };
60
74
 
61
75
  export type ParsedResult = {
76
+ id?: string;
77
+ lang?: string;
62
78
  mountable?: boolean;
63
79
  root?: string;
64
80
  selector?: string;
@@ -68,3 +84,77 @@ export type ParsedResult = {
68
84
  templates: Record<string, string>;
69
85
  };
70
86
 
87
+ export type Doc = {
88
+ id?: string;
89
+ lang?: string;
90
+ meta: Readonly<Record<string, unknown>>;
91
+ serializerConfig?: SerializerConfig;
92
+ allowAll(): void;
93
+ allowAttributes(attributes: string[]): void;
94
+ allowElements(elements: Record<string, SerializationElement>): void;
95
+ };
96
+
97
+ export type SimplifiedElement = {
98
+ id?: string;
99
+ tag: string;
100
+ class?: string;
101
+ attrs: Record<string, string | boolean | null>;
102
+ };
103
+
104
+ export type GlobalCtx = {
105
+ doc: Doc;
106
+ };
107
+
108
+ export type BeforeRenderCtx = {
109
+ inlineArg?: string;
110
+ blockArg?: string;
111
+ el: SimplifiedElement;
112
+ chain: SimplifiedElement[];
113
+ doc: Readonly<Doc>;
114
+ //serializerConfig?: SerializerConfig;
115
+ //setMeta(key: string, value: unknown): void;
116
+ //allowAll(): void;
117
+ //allowAttributes(attributes: string[]): void;
118
+ //allowElements(elements: Record<string, SerializationElement>): void;
119
+ };
120
+
121
+ export type AttrValueCtx = {
122
+ tag: string;
123
+ doc: Readonly<Doc>;
124
+ meta: Readonly<Record<string, string>>
125
+ };
126
+
127
+ export type AppliesTo =
128
+ | 'self'
129
+ | 'direct-descendants'
130
+ | 'children'
131
+ | 'all'
132
+ ;
133
+
134
+ export type DirectiveDef = {
135
+ onGlobal?: (ctx: GlobalCtx) => void;
136
+ beforeFragment?: () => void;
137
+ beforeElement?: () => void;
138
+ beforeRender?: (ctx: BeforeRenderCtx) => void;
139
+ render?: (inlineArgs?: string, blockArgs?: string) => string;
140
+ renderAttr?: (ctx: AttrValueCtx) => string;
141
+ };
142
+
143
+ export type AppliedDirective = {
144
+ inlineArg?: string;
145
+ blockArg?: string;
146
+ onGlobal?: (ctx: GlobalCtx) => void;
147
+ beforeFragment?: () => void;
148
+ beforeElement?: () => void;
149
+ beforeRender?: (ctx: BeforeRenderCtx) => void;
150
+ render?: (inlineArgs?: string, blockArgs?: string) => string;
151
+ renderAttr?: (ctx: AttrValueCtx) => string;
152
+ }
153
+
154
+ export type LongformArgs = {
155
+ key?: string;
156
+ allowAll?: boolean;
157
+ allowedAttributes?: string[];
158
+ allowedElements?: Array<string | SerializationElement>;
159
+ directives?: Record<string, DirectiveDef>;
160
+ };
package/package.json CHANGED
@@ -4,10 +4,22 @@
4
4
  "author": "Matthew Quinn",
5
5
  "homepage": "https://github.com/occultist-dev/longform",
6
6
  "license": "MIT",
7
- "version": "0.0.7",
7
+ "version": "0.0.9",
8
8
  "type": "module",
9
- "main": "dist/longform.js",
10
- "types": "dist/mod.d.ts",
9
+ "keywords": [
10
+ "HTML",
11
+ "Fragments",
12
+ "Templating",
13
+ "Longform"
14
+ ],
15
+ "main": "./dist/longform.js",
16
+ "types": "./dist/mod.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/longform.js",
20
+ "require": "./dist/longform.cjs"
21
+ }
22
+ },
11
23
  "repository": {
12
24
  "type": "git",
13
25
  "url": "https://github.com/occultist-dev/longform"
@@ -24,12 +36,6 @@
24
36
  "url": "https://github.com/occultist-dev/longform/blob/main/LICENSE"
25
37
  }
26
38
  ],
27
- "keywords": [
28
- "HTML",
29
- "Fragments",
30
- "Templating",
31
- "Longform"
32
- ],
33
39
  "devDependencies": {
34
40
  "@rollup/plugin-typescript": "^12.3.0",
35
41
  "@types/commonmark": "^0.27.10",
@@ -37,14 +43,14 @@
37
43
  "@types/jsdom": "^27.0.0",
38
44
  "@types/node": "^24.8.1",
39
45
  "commonmark": "^0.31.2",
40
- "dompurify": "^3.3.0",
46
+ "dompurify": "^3.4.1",
41
47
  "jsdom": "^27.0.1",
42
48
  "marked": "^16.4.1",
43
49
  "prettier": "^3.6.2",
44
- "rollup": "^4.53.3",
50
+ "rollup": "^4.60.2",
45
51
  "tslib": "^2.8.1",
46
52
  "typescript": "^5.9.3",
47
- "vnu-jar": "^24.10.17"
53
+ "vnu-jar": "^26.4.16"
48
54
  },
49
55
  "files": [
50
56
  "package.json",
@@ -56,7 +62,7 @@
56
62
  "scripts": {
57
63
  "build": "node build.ts",
58
64
  "bench": "deno bench --no-check --allow-read ./bench/longform.bench.ts",
59
- "test": "node --test lib/longform.test.ts",
65
+ "test": "node --test test/**/*.test.ts",
60
66
  "serve": "node ./serve.ts"
61
67
  }
62
68
  }