@longform/longform 0.0.11 → 0.0.13

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
@@ -24,6 +24,7 @@ const sniffTestRe = /^((?: )*)(?:(@)([a-z][a-z\-]*(?::[a-z][a-z\-]*)?)(?:(?:::
24
24
  // captures each id, class and attribute declaration in an element
25
25
  , inner = /(?:\.([a-z][\w\-]+)|#([a-z][\w\-]+)|\[([a-z][a-z\-]+(?::[a-z][a-z|\-]*)?)(?:=(?:"([^"]+)"|'([^']+)'|([^\]]+)))?\])/gi
26
26
  , attribute1 = /((?:\ \ )+)\[(\w[\w-]*(?::\w[\w-]*)?)(?:=([^\n]+))?\]/
27
+ , directiveAttr = /^@([a-z][\w\-]*(?::[a-z][\w\-]*))$/
27
28
  , preformattedClose = /[ \t]*}}?[ \t]*/
28
29
  , id1 = /((?:\ \ )+)?#(#)?([\w\-]+)(?: ([\["]))?/gmi
29
30
  , identRe = /^(\ \ )+/
@@ -36,7 +37,8 @@ const sniffTestRe = /^((?: )*)(?:(@)([a-z][a-z\-]*(?::[a-z][a-z\-]*)?)(?:(?:::
36
37
 
37
38
  let m1: RegExpExecArray | null
38
39
  , m2: RegExpExecArray | null
39
- , m3: RegExpExecArray | null;
40
+ , m3: RegExpExecArray | null
41
+ , m4: RegExpExecArray | null
40
42
 
41
43
  const entities = {
42
44
  '&': '&',
@@ -69,7 +71,7 @@ function makeElement(indent: number = 0): WorkingElement {
69
71
  html: '',
70
72
  mount: undefined,
71
73
  serializerConfig: undefined,
72
- chain: undefined,
74
+ chain: undefined as unknown as WorkingElement[],
73
75
  beforeRender: undefined,
74
76
  };
75
77
  }
@@ -90,13 +92,14 @@ class Doc {
90
92
  id?: string;
91
93
  lang?: string;
92
94
  dir?: string;
93
- meta: Record<string, unknown> = {};
95
+ meta: Readonly<Record<string, unknown>>;
94
96
  #serializerConfig: SerializerConfig;
95
97
 
96
98
  constructor(
97
- id?: string | undefined,
98
- lang?: string | undefined,
99
- dir?: string | undefined,
99
+ id: string | undefined,
100
+ lang: string | undefined,
101
+ dir: string | undefined,
102
+ meta: Record<string, unknown>,
100
103
  allowAll?: boolean | undefined,
101
104
  allowedAttributes?: string[] | undefined,
102
105
  allowedElements?: Array<string | SerializationElement>,
@@ -104,6 +107,7 @@ class Doc {
104
107
  this.id = id;
105
108
  this.lang = lang;
106
109
  this.dir = dir;
110
+ this.meta = Object.freeze(meta);
107
111
  this.#serializerConfig = {
108
112
  allowAll: allowAll ?? false,
109
113
  allowedAttributes: allowedAttributes ?? [],
@@ -162,9 +166,10 @@ export function longform(
162
166
  , fragment: WorkingFragment = makeFragment()
163
167
  // the root fragment
164
168
  , root: WorkingFragment | null = null
165
- , id: string | undefined
166
- , lang: string | undefined
167
- , dir: string | undefined
169
+ , id: string | undefined = args?.id
170
+ , lang: string | undefined = args?.lang
171
+ , dir: string | undefined = args?.dir
172
+ , meta: Record<string, unknown> = {}
168
173
  , doc: Doc | undefined
169
174
  // ids of claimed fragments
170
175
  const claimed: Set<string> = new Set()
@@ -175,6 +180,17 @@ export function longform(
175
180
  , output: ParsedResult = Object.create(null)
176
181
  , directives: Record<string, DirectiveDef> = {}
177
182
 
183
+ let key: string = args?.key as string;
184
+
185
+ if (!args?.predictable && key == null) {
186
+ const arr = new Uint8Array(10);
187
+
188
+ crypto.getRandomValues(arr)
189
+
190
+ key = arr.toHex();
191
+ } else if (key == null) {
192
+ key = 'lf';
193
+ }
178
194
 
179
195
  output.fragments = {};
180
196
  output.templates = {};
@@ -201,7 +217,7 @@ export function longform(
201
217
  if (Array.isArray(element.beforeRender)) {
202
218
  const el: SimplifiedElement = {
203
219
  id: element.id,
204
- tag: element.tag,
220
+ tag: element.tag as string,
205
221
  class: element.class,
206
222
  attrs: element.attrs,
207
223
  };
@@ -210,7 +226,7 @@ export function longform(
210
226
  for (let i = 0, l = element.chain.length, el = element.chain[i]; i < l; i++) {
211
227
  chain.push({
212
228
  id: el.id,
213
- tag: el.tag,
229
+ tag: el.tag as string,
214
230
  class: el.class,
215
231
  attrs: el.attrs,
216
232
  });
@@ -220,7 +236,7 @@ export function longform(
220
236
  def.beforeRender({
221
237
  el,
222
238
  chain,
223
- doc,
239
+ doc: doc as Doc,
224
240
  inlineArg: def.inlineArg,
225
241
  blockArg: def.blockArg,
226
242
  });
@@ -235,18 +251,6 @@ export function longform(
235
251
 
236
252
  fragment.html += `<${element.tag}`
237
253
 
238
- if (root) {
239
- if (fragment.type === 'root') {
240
- fragment.html += ` data-lf-root`;
241
- } else if (fragment.type === 'bare' || fragment.type === 'range') {
242
- fragment.html += ` data-lf="${fragment.id}"`;
243
- }
244
- }
245
-
246
- if (element.mount !== undefined) {
247
- fragment.html += ` data-lf-mount="${element.mount}"`;
248
- }
249
-
250
254
  if (element.id !== undefined) {
251
255
  fragment.html += ' id="' + element.id + '"';
252
256
  }
@@ -256,6 +260,7 @@ export function longform(
256
260
  }
257
261
 
258
262
  for (const attr of Object.entries(element.attrs)) {
263
+ if (attr[0] === 'id' || attr[0] === 'class') continue;
259
264
  if (attr[1] == null) {
260
265
  fragment.html += ' ' + attr[0]
261
266
  } else {
@@ -263,6 +268,20 @@ export function longform(
263
268
  }
264
269
  }
265
270
 
271
+ if (root) {
272
+ if (fragment.type === 'root') {
273
+ fragment.html += ` data-${key}-root`;
274
+ } else if (fragment.type === 'bare' || fragment.type === 'range') {
275
+ fragment.html += ` data-${key}="${fragment.id}"`;
276
+ } else if (fragment.type === 'embed' && !args?.predictable) {
277
+ fragment.html += ` data-${key}="${fragment.id}"`;
278
+ }
279
+ }
280
+
281
+ if (element.mount !== undefined) {
282
+ fragment.html += ` data-${key}-mount="${element.mount}"`;
283
+ }
284
+
266
285
  fragment.html += '>';
267
286
 
268
287
  if (Array.isArray(element.chain)) {
@@ -313,7 +332,7 @@ export function longform(
313
332
  fragment.els[fragment.els.length - 1].indent !== targetIndent - 1
314
333
  )
315
334
  ) {
316
- const element = fragment.els.pop();
335
+ const element = fragment.els.pop() as WorkingElement;
317
336
 
318
337
  if (Array.isArray(element.chain)) {
319
338
  for (let i = 0, l = element.chain.length; i < l; i++) {
@@ -349,7 +368,7 @@ export function longform(
349
368
 
350
369
  // If this is a script tag or preformatted block
351
370
  // we want to retain the intended formatting less
352
- // the indent. Preformatting can apply to any element
371
+ // the indent. Pre-formatting can apply to any element
353
372
  // by ending the declaration with `:: {`.
354
373
  if (verbatimIndent != null) {
355
374
  // inside a script or preformatted block
@@ -404,21 +423,45 @@ export function longform(
404
423
 
405
424
  switch (m1[DIRECTIVE]) {
406
425
  case 'id': {
407
- // TODO: Add id validation
408
- id = inlineArgs.trim();
426
+ id = id ?? new URL(inlineArgs.trim(), args?.base).toString();
409
427
  continue main;
410
428
  }
411
429
  case 'lang': {
412
- lang = inlineArgs.trim();
430
+ lang = lang ?? inlineArgs.trim();
413
431
  continue main;
414
432
  }
415
433
  case 'dir': {
416
- dir = inlineArgs.trim();
434
+ dir = dir ?? inlineArgs.trim();
417
435
  continue main;
418
436
  }
437
+ default: {
438
+ const def = directives[m1[DIRECTIVE]];
439
+
440
+ if (typeof def?.meta === 'function') {
441
+ if (Object.keys(def).length > 1) {
442
+ throw new Error(
443
+ `A custom directive performing the meta role cannot be used for other purposes. ` +
444
+ `See @${m1[DIRECTIVE]}`,
445
+ );
446
+ }
447
+
448
+ meta[m1[DIRECTIVE]] = def.meta({ inlineArgs });
449
+ continue main;
450
+ }
451
+ }
419
452
  }
420
453
 
421
- doc = new Doc(id, lang, dir, args?.allowAll, args?.allowedAttributes, args?.allowedElements);
454
+ doc = new Doc(id, lang, dir, meta, args?.allowAll, args?.allowedAttributes, args?.allowedElements);
455
+
456
+ if (args?.outputMode === 'meta') {
457
+ return {
458
+ key,
459
+ id,
460
+ lang,
461
+ dir,
462
+ meta: doc.meta,
463
+ };
464
+ }
422
465
  }
423
466
 
424
467
  switch (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) {
@@ -611,7 +654,17 @@ export function longform(
611
654
  }
612
655
  break;
613
656
  default:
614
- working.attrs[m3[3]] = value;
657
+ m4 = directiveAttr.exec(m3[3]);
658
+
659
+ if (m4 != null) {
660
+ const def = directives[m4[1]];
661
+
662
+ if (def != null && typeof def.attr === 'function') {
663
+ working.attrs[m3[3]] = def.attr({ doc, tag: element.tag as string });
664
+ }
665
+ } else {
666
+ working.attrs[m3[3]] = value;
667
+ }
615
668
  }
616
669
  }
617
670
  }
@@ -751,7 +804,7 @@ export function longform(
751
804
  if (i === 0 && root == null) {
752
805
  continue;
753
806
  } else if (i === 0) {
754
- fragment = root;
807
+ fragment = root as WorkingFragment;
755
808
  } else {
756
809
  fragment = arr[i - 1];
757
810
  }
@@ -765,41 +818,44 @@ export function longform(
765
818
 
766
819
  if (root?.html != null) {
767
820
  output.root = root.html;
768
- output.selector = `[data-lf-root]`;
821
+ output.selector = `[data-${key}-root]`;
769
822
  }
770
823
 
771
824
  for (let i = 0; i < arr.length; i++) {
772
- let selector: string;
773
- const fragment = arr[i];
825
+ let selector!: string;
826
+ const fragment: WorkingFragment = arr[i];
774
827
 
775
- if (fragment == null || claimed.has(fragment.id)) {
828
+ if (fragment == null || claimed.has(fragment.id as string)) {
776
829
  continue;
777
830
  }
778
831
 
779
- if (fragment.type === 'embed') {
780
- selector = `[id=${fragment.id}]`;
781
- } else if (fragment.type === 'bare') {
782
- selector = `[data-lf=${fragment.id}]`;
783
- } else if (fragment.type === 'range') {
784
- selector = `[data-lf=${fragment.id}]`;
832
+ switch (fragment.type) {
833
+ case 'embed': {
834
+ if (args?.predictable) {
835
+ selector = `[id=${fragment.id}]`;
836
+ break;
837
+ }
838
+ }
839
+ case 'bare':
840
+ case 'range': selector = `[data-${key}=${fragment.id}]`;
785
841
  }
786
842
 
787
- output.fragments[fragment.id] = {
788
- id: fragment.id,
843
+ output.fragments[fragment.id as string] = {
844
+ id: fragment.id as string,
789
845
  selector,
790
846
  type: fragment.type as 'embed' | 'bare' | 'range',
791
847
  html: fragment.html,
792
848
  };
793
849
  }
794
850
 
851
+ output.key = key;
795
852
  output.id = doc?.id;
796
853
  output.lang = doc?.lang;
854
+ output.dir = doc?.dir;
797
855
 
798
856
  return output;
799
857
  }
800
858
 
801
-
802
-
803
859
  /**
804
860
  * Processes a client side Longform template to HTML fragment string.
805
861
  *
@@ -827,3 +883,4 @@ export function processTemplate(
827
883
 
828
884
  return Object.values(longform(lf).fragments)[0]?.html ?? null;
829
885
  }
886
+
package/lib/types.ts CHANGED
@@ -72,18 +72,6 @@ export type MountPoint = {
72
72
  part: string;
73
73
  };
74
74
 
75
- export type ParsedResult = {
76
- id?: string;
77
- lang?: string;
78
- mountable?: boolean;
79
- root?: string;
80
- selector?: string;
81
- mountPoints: MountPoint[];
82
- tail?: string;
83
- fragments: Record<string, Fragment>;
84
- templates: Record<string, string>;
85
- };
86
-
87
75
  export type Doc = {
88
76
  id?: string;
89
77
  lang?: string;
@@ -105,6 +93,10 @@ export type GlobalCtx = {
105
93
  doc: Doc;
106
94
  };
107
95
 
96
+ export type MetaCtx = {
97
+ inlineArgs?: string;
98
+ }
99
+
108
100
  export type BeforeRenderCtx = {
109
101
  inlineArg?: string;
110
102
  blockArg?: string;
@@ -118,10 +110,9 @@ export type BeforeRenderCtx = {
118
110
  //allowElements(elements: Record<string, SerializationElement>): void;
119
111
  };
120
112
 
121
- export type AttrValueCtx = {
113
+ export type AttrCtx = {
122
114
  tag: string;
123
115
  doc: Readonly<Doc>;
124
- meta: Readonly<Record<string, string>>
125
116
  };
126
117
 
127
118
  export type AppliesTo =
@@ -132,12 +123,18 @@ export type AppliesTo =
132
123
  ;
133
124
 
134
125
  export type DirectiveDef = {
135
- onGlobal?: (ctx: GlobalCtx) => void;
126
+ /**
127
+ * A directive that implements the meta hook cannot be used for other purposes.
128
+ * This is enforced by the parser through checking the amount of keys on the
129
+ * definition object.
130
+ */
131
+ meta?: (args: MetaCtx) => unknown;
132
+ global?: (ctx: GlobalCtx) => void;
136
133
  beforeFragment?: () => void;
137
134
  beforeElement?: () => void;
138
135
  beforeRender?: (ctx: BeforeRenderCtx) => void;
139
136
  render?: (inlineArgs?: string, blockArgs?: string) => string;
140
- renderAttr?: (ctx: AttrValueCtx) => string;
137
+ attr?: (ctx: AttrCtx) => string;
141
138
  };
142
139
 
143
140
  export type AppliedDirective = {
@@ -148,14 +145,85 @@ export type AppliedDirective = {
148
145
  beforeElement?: () => void;
149
146
  beforeRender?: (ctx: BeforeRenderCtx) => void;
150
147
  render?: (inlineArgs?: string, blockArgs?: string) => string;
151
- renderAttr?: (ctx: AttrValueCtx) => string;
148
+ attr?: (ctx: AttrCtx) => string;
152
149
  }
153
150
 
151
+ export type OutputMode =
152
+ | 'doc'
153
+ | 'meta'
154
+ | 'mountable'
155
+ ;
156
+
154
157
  export type LongformArgs = {
158
+
159
+ /**
160
+ * The key used for creating data attributes.
161
+ * Defaults to a random string or 'lf' if predictable mode is enabled.
162
+ */
155
163
  key?: string;
164
+
165
+ /**
166
+ * If true the parser will default to using the 'lf' key for data attributes
167
+ * and fragments with embedded ids will not used data attributes for selectors.
168
+ *
169
+ * This is less safe in environments where Longform is used for SSR.
170
+ */
171
+ predictable?: boolean;
172
+
173
+ /**
174
+ * The id of the document. If set the @id is ignored.
175
+ * This can be used when creating partial re-usable documents.
176
+ */
177
+ id?: string;
178
+
179
+ /**
180
+ * The language tag of the document. If set the @lang
181
+ * directive is ignored.
182
+ * This can be used when creating partial re-usable documents.
183
+ */
184
+ lang?: string;
185
+
186
+ /**
187
+ * The text direction of the document. If set the @dir
188
+ * directive is ignored.
189
+ * This can be used when creating partial re-usable documents.
190
+ */
191
+ dir?: string;
192
+
193
+ /**
194
+ * A base URL that can be used for resolving relative URLs
195
+ * declared using the @id directive.
196
+ */
197
+ base?: string;
198
+
199
+ /**
200
+ * Metadata set on the document.
201
+ */
202
+ meta?: Record<string, unknown>;
203
+
204
+ /**
205
+ * The output mode of the document.
206
+ */
207
+ outputMode?: OutputMode;
208
+
156
209
  allowAll?: boolean;
157
210
  allowedAttributes?: string[];
158
211
  allowedElements?: Array<string | SerializationElement>;
159
212
  directives?: Record<string, DirectiveDef>;
160
213
  fragments?: Record<string, Fragment>;
161
214
  };
215
+
216
+ export type ParsedResult = {
217
+ key: string;
218
+ id?: string;
219
+ lang?: string;
220
+ dir?: string;
221
+ meta: Record<string, unknown>;
222
+ mountable?: boolean;
223
+ root?: string;
224
+ selector?: string;
225
+ mountPoints: MountPoint[];
226
+ tail?: string;
227
+ fragments: Record<string, Fragment>;
228
+ templates: Record<string, string>;
229
+ };
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.11",
7
+ "version": "0.0.13",
8
8
  "type": "module",
9
9
  "keywords": [
10
10
  "HTML",
@@ -49,7 +49,7 @@
49
49
  "prettier": "^3.6.2",
50
50
  "rollup": "^4.60.2",
51
51
  "tslib": "^2.8.1",
52
- "typescript": "^5.9.3",
52
+ "typescript": "^6.0.3",
53
53
  "vnu-jar": "^26.4.16"
54
54
  },
55
55
  "files": [