@longform/longform 0.0.7 → 0.0.8

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