@longform/longform 0.0.11 → 0.0.12

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
@@ -69,7 +69,7 @@ function makeElement(indent: number = 0): WorkingElement {
69
69
  html: '',
70
70
  mount: undefined,
71
71
  serializerConfig: undefined,
72
- chain: undefined,
72
+ chain: undefined as unknown as WorkingElement[],
73
73
  beforeRender: undefined,
74
74
  };
75
75
  }
@@ -90,13 +90,14 @@ class Doc {
90
90
  id?: string;
91
91
  lang?: string;
92
92
  dir?: string;
93
- meta: Record<string, unknown> = {};
93
+ meta: Readonly<Record<string, unknown>>;
94
94
  #serializerConfig: SerializerConfig;
95
95
 
96
96
  constructor(
97
- id?: string | undefined,
98
- lang?: string | undefined,
99
- dir?: string | undefined,
97
+ id: string | undefined,
98
+ lang: string | undefined,
99
+ dir: string | undefined,
100
+ meta: Record<string, unknown>,
100
101
  allowAll?: boolean | undefined,
101
102
  allowedAttributes?: string[] | undefined,
102
103
  allowedElements?: Array<string | SerializationElement>,
@@ -104,6 +105,7 @@ class Doc {
104
105
  this.id = id;
105
106
  this.lang = lang;
106
107
  this.dir = dir;
108
+ this.meta = Object.freeze(meta);
107
109
  this.#serializerConfig = {
108
110
  allowAll: allowAll ?? false,
109
111
  allowedAttributes: allowedAttributes ?? [],
@@ -162,9 +164,10 @@ export function longform(
162
164
  , fragment: WorkingFragment = makeFragment()
163
165
  // the root fragment
164
166
  , root: WorkingFragment | null = null
165
- , id: string | undefined
166
- , lang: string | undefined
167
- , dir: string | undefined
167
+ , id: string | undefined = args?.id
168
+ , lang: string | undefined = args?.lang
169
+ , dir: string | undefined = args?.dir
170
+ , meta: Record<string, unknown> = {}
168
171
  , doc: Doc | undefined
169
172
  // ids of claimed fragments
170
173
  const claimed: Set<string> = new Set()
@@ -175,6 +178,17 @@ export function longform(
175
178
  , output: ParsedResult = Object.create(null)
176
179
  , directives: Record<string, DirectiveDef> = {}
177
180
 
181
+ let key: string = args?.key as string;
182
+
183
+ if (!args?.predictable && key == null) {
184
+ const arr = new Uint8Array(10);
185
+
186
+ crypto.getRandomValues(arr)
187
+
188
+ key = arr.toHex();
189
+ } else if (key == null) {
190
+ key = 'lf';
191
+ }
178
192
 
179
193
  output.fragments = {};
180
194
  output.templates = {};
@@ -201,7 +215,7 @@ export function longform(
201
215
  if (Array.isArray(element.beforeRender)) {
202
216
  const el: SimplifiedElement = {
203
217
  id: element.id,
204
- tag: element.tag,
218
+ tag: element.tag as string,
205
219
  class: element.class,
206
220
  attrs: element.attrs,
207
221
  };
@@ -210,7 +224,7 @@ export function longform(
210
224
  for (let i = 0, l = element.chain.length, el = element.chain[i]; i < l; i++) {
211
225
  chain.push({
212
226
  id: el.id,
213
- tag: el.tag,
227
+ tag: el.tag as string,
214
228
  class: el.class,
215
229
  attrs: el.attrs,
216
230
  });
@@ -220,7 +234,7 @@ export function longform(
220
234
  def.beforeRender({
221
235
  el,
222
236
  chain,
223
- doc,
237
+ doc: doc as Doc,
224
238
  inlineArg: def.inlineArg,
225
239
  blockArg: def.blockArg,
226
240
  });
@@ -235,18 +249,6 @@ export function longform(
235
249
 
236
250
  fragment.html += `<${element.tag}`
237
251
 
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
252
  if (element.id !== undefined) {
251
253
  fragment.html += ' id="' + element.id + '"';
252
254
  }
@@ -256,6 +258,7 @@ export function longform(
256
258
  }
257
259
 
258
260
  for (const attr of Object.entries(element.attrs)) {
261
+ if (attr[0] === 'id' || attr[0] === 'class') continue;
259
262
  if (attr[1] == null) {
260
263
  fragment.html += ' ' + attr[0]
261
264
  } else {
@@ -263,6 +266,20 @@ export function longform(
263
266
  }
264
267
  }
265
268
 
269
+ if (root) {
270
+ if (fragment.type === 'root') {
271
+ fragment.html += ` data-${key}-root`;
272
+ } else if (fragment.type === 'bare' || fragment.type === 'range') {
273
+ fragment.html += ` data-${key}="${fragment.id}"`;
274
+ } else if (fragment.type === 'embed' && !args?.predictable) {
275
+ fragment.html += ` data-${key}="${fragment.id}"`;
276
+ }
277
+ }
278
+
279
+ if (element.mount !== undefined) {
280
+ fragment.html += ` data-${key}-mount="${element.mount}"`;
281
+ }
282
+
266
283
  fragment.html += '>';
267
284
 
268
285
  if (Array.isArray(element.chain)) {
@@ -313,7 +330,7 @@ export function longform(
313
330
  fragment.els[fragment.els.length - 1].indent !== targetIndent - 1
314
331
  )
315
332
  ) {
316
- const element = fragment.els.pop();
333
+ const element = fragment.els.pop() as WorkingElement;
317
334
 
318
335
  if (Array.isArray(element.chain)) {
319
336
  for (let i = 0, l = element.chain.length; i < l; i++) {
@@ -349,7 +366,7 @@ export function longform(
349
366
 
350
367
  // If this is a script tag or preformatted block
351
368
  // we want to retain the intended formatting less
352
- // the indent. Preformatting can apply to any element
369
+ // the indent. Pre-formatting can apply to any element
353
370
  // by ending the declaration with `:: {`.
354
371
  if (verbatimIndent != null) {
355
372
  // inside a script or preformatted block
@@ -404,21 +421,45 @@ export function longform(
404
421
 
405
422
  switch (m1[DIRECTIVE]) {
406
423
  case 'id': {
407
- // TODO: Add id validation
408
- id = inlineArgs.trim();
424
+ id = id ?? new URL(inlineArgs.trim(), args?.base).toString();
409
425
  continue main;
410
426
  }
411
427
  case 'lang': {
412
- lang = inlineArgs.trim();
428
+ lang = lang ?? inlineArgs.trim();
413
429
  continue main;
414
430
  }
415
431
  case 'dir': {
416
- dir = inlineArgs.trim();
432
+ dir = dir ?? inlineArgs.trim();
417
433
  continue main;
418
434
  }
435
+ default: {
436
+ const def = directives[m1[DIRECTIVE]];
437
+
438
+ if (typeof def?.meta === 'function') {
439
+ if (Object.keys(def).length > 1) {
440
+ throw new Error(
441
+ `A custom directive performing the meta role cannot be used for other purposes. ` +
442
+ `See @${m1[DIRECTIVE]}`,
443
+ );
444
+ }
445
+
446
+ meta[m1[DIRECTIVE]] = def.meta({ inlineArgs });
447
+ continue main;
448
+ }
449
+ }
419
450
  }
420
451
 
421
- doc = new Doc(id, lang, dir, args?.allowAll, args?.allowedAttributes, args?.allowedElements);
452
+ doc = new Doc(id, lang, dir, meta, args?.allowAll, args?.allowedAttributes, args?.allowedElements);
453
+
454
+ if (args?.outputMode === 'meta') {
455
+ return {
456
+ key,
457
+ id,
458
+ lang,
459
+ dir,
460
+ meta: doc.meta,
461
+ };
462
+ }
422
463
  }
423
464
 
424
465
  switch (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) {
@@ -751,7 +792,7 @@ export function longform(
751
792
  if (i === 0 && root == null) {
752
793
  continue;
753
794
  } else if (i === 0) {
754
- fragment = root;
795
+ fragment = root as WorkingFragment;
755
796
  } else {
756
797
  fragment = arr[i - 1];
757
798
  }
@@ -765,41 +806,44 @@ export function longform(
765
806
 
766
807
  if (root?.html != null) {
767
808
  output.root = root.html;
768
- output.selector = `[data-lf-root]`;
809
+ output.selector = `[data-${key}-root]`;
769
810
  }
770
811
 
771
812
  for (let i = 0; i < arr.length; i++) {
772
- let selector: string;
773
- const fragment = arr[i];
813
+ let selector!: string;
814
+ const fragment: WorkingFragment = arr[i];
774
815
 
775
- if (fragment == null || claimed.has(fragment.id)) {
816
+ if (fragment == null || claimed.has(fragment.id as string)) {
776
817
  continue;
777
818
  }
778
819
 
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}]`;
820
+ switch (fragment.type) {
821
+ case 'embed': {
822
+ if (args?.predictable) {
823
+ selector = `[id=${fragment.id}]`;
824
+ break;
825
+ }
826
+ }
827
+ case 'bare':
828
+ case 'range': selector = `[data-${key}=${fragment.id}]`;
785
829
  }
786
830
 
787
- output.fragments[fragment.id] = {
788
- id: fragment.id,
831
+ output.fragments[fragment.id as string] = {
832
+ id: fragment.id as string,
789
833
  selector,
790
834
  type: fragment.type as 'embed' | 'bare' | 'range',
791
835
  html: fragment.html,
792
836
  };
793
837
  }
794
838
 
839
+ output.key = key;
795
840
  output.id = doc?.id;
796
841
  output.lang = doc?.lang;
842
+ output.dir = doc?.dir;
797
843
 
798
844
  return output;
799
845
  }
800
846
 
801
-
802
-
803
847
  /**
804
848
  * Processes a client side Longform template to HTML fragment string.
805
849
  *
@@ -827,3 +871,4 @@ export function processTemplate(
827
871
 
828
872
  return Object.values(longform(lf).fragments)[0]?.html ?? null;
829
873
  }
874
+
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;
@@ -132,7 +124,13 @@ export type AppliesTo =
132
124
  ;
133
125
 
134
126
  export type DirectiveDef = {
135
- onGlobal?: (ctx: GlobalCtx) => void;
127
+ /**
128
+ * A directive that implements the meta hook cannot be used for other purposes.
129
+ * This is enforced by the parser through checking the amount of keys on the
130
+ * definition object.
131
+ */
132
+ meta?: (args: MetaCtx) => unknown;
133
+ global?: (ctx: GlobalCtx) => void;
136
134
  beforeFragment?: () => void;
137
135
  beforeElement?: () => void;
138
136
  beforeRender?: (ctx: BeforeRenderCtx) => void;
@@ -151,11 +149,82 @@ export type AppliedDirective = {
151
149
  renderAttr?: (ctx: AttrValueCtx) => string;
152
150
  }
153
151
 
152
+ export type OutputMode =
153
+ | 'doc'
154
+ | 'meta'
155
+ | 'mountable'
156
+ ;
157
+
154
158
  export type LongformArgs = {
159
+
160
+ /**
161
+ * The key used for creating data attributes.
162
+ * Defaults to a random string or 'lf' if predictable mode is enabled.
163
+ */
155
164
  key?: string;
165
+
166
+ /**
167
+ * If true the parser will default to using the 'lf' key for data attributes
168
+ * and fragments with embedded ids will not used data attributes for selectors.
169
+ *
170
+ * This is less safe in environments where Longform is used for SSR.
171
+ */
172
+ predictable?: boolean;
173
+
174
+ /**
175
+ * The id of the document. If set the @id is ignored.
176
+ * This can be used when creating partial re-usable documents.
177
+ */
178
+ id?: string;
179
+
180
+ /**
181
+ * The language tag of the document. If set the @lang
182
+ * directive is ignored.
183
+ * This can be used when creating partial re-usable documents.
184
+ */
185
+ lang?: string;
186
+
187
+ /**
188
+ * The text direction of the document. If set the @dir
189
+ * directive is ignored.
190
+ * This can be used when creating partial re-usable documents.
191
+ */
192
+ dir?: string;
193
+
194
+ /**
195
+ * A base URL that can be used for resolving relative URLs
196
+ * declared using the @id directive.
197
+ */
198
+ base?: string;
199
+
200
+ /**
201
+ * Metadata set on the document.
202
+ */
203
+ meta?: Record<string, unknown>;
204
+
205
+ /**
206
+ * The output mode of the document.
207
+ */
208
+ outputMode?: OutputMode;
209
+
156
210
  allowAll?: boolean;
157
211
  allowedAttributes?: string[];
158
212
  allowedElements?: Array<string | SerializationElement>;
159
213
  directives?: Record<string, DirectiveDef>;
160
214
  fragments?: Record<string, Fragment>;
161
215
  };
216
+
217
+ export type ParsedResult = {
218
+ key: string;
219
+ id?: string;
220
+ lang?: string;
221
+ dir?: string;
222
+ meta: Record<string, unknown>;
223
+ mountable?: boolean;
224
+ root?: string;
225
+ selector?: string;
226
+ mountPoints: MountPoint[];
227
+ tail?: string;
228
+ fragments: Record<string, Fragment>;
229
+ templates: Record<string, string>;
230
+ };
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.12",
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": [