@longform/longform 0.0.10 → 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,20 +164,34 @@ 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()
171
174
  // parsed fragments
172
- , parsed: Map<string, WorkingFragment> = new Map()
175
+ , parsed: Map<string, WorkingFragment> = new Map(
176
+ Object.entries(args?.fragments as unknown as Record<string, WorkingFragment> ?? {})
177
+ )
173
178
  , output: ParsedResult = Object.create(null)
174
179
  , directives: Record<string, DirectiveDef> = {}
175
180
 
181
+ let key: string = args?.key as string;
176
182
 
177
- output.fragments = Object.assign({}, args?.fragments);
178
- output.templates = Object.create(null);
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
+ }
192
+
193
+ output.fragments = {};
194
+ output.templates = {};
179
195
 
180
196
  if (args?.directives != null) {
181
197
  const entries = Object.entries(args.directives);
@@ -199,7 +215,7 @@ export function longform(
199
215
  if (Array.isArray(element.beforeRender)) {
200
216
  const el: SimplifiedElement = {
201
217
  id: element.id,
202
- tag: element.tag,
218
+ tag: element.tag as string,
203
219
  class: element.class,
204
220
  attrs: element.attrs,
205
221
  };
@@ -208,7 +224,7 @@ export function longform(
208
224
  for (let i = 0, l = element.chain.length, el = element.chain[i]; i < l; i++) {
209
225
  chain.push({
210
226
  id: el.id,
211
- tag: el.tag,
227
+ tag: el.tag as string,
212
228
  class: el.class,
213
229
  attrs: el.attrs,
214
230
  });
@@ -218,7 +234,7 @@ export function longform(
218
234
  def.beforeRender({
219
235
  el,
220
236
  chain,
221
- doc,
237
+ doc: doc as Doc,
222
238
  inlineArg: def.inlineArg,
223
239
  blockArg: def.blockArg,
224
240
  });
@@ -233,18 +249,6 @@ export function longform(
233
249
 
234
250
  fragment.html += `<${element.tag}`
235
251
 
236
- if (root) {
237
- if (fragment.type === 'root') {
238
- fragment.html += ` data-lf-root`;
239
- } else if (fragment.type === 'bare' || fragment.type === 'range') {
240
- fragment.html += ` data-lf="${fragment.id}"`;
241
- }
242
- }
243
-
244
- if (element.mount !== undefined) {
245
- fragment.html += ` data-lf-mount="${element.mount}"`;
246
- }
247
-
248
252
  if (element.id !== undefined) {
249
253
  fragment.html += ' id="' + element.id + '"';
250
254
  }
@@ -254,6 +258,7 @@ export function longform(
254
258
  }
255
259
 
256
260
  for (const attr of Object.entries(element.attrs)) {
261
+ if (attr[0] === 'id' || attr[0] === 'class') continue;
257
262
  if (attr[1] == null) {
258
263
  fragment.html += ' ' + attr[0]
259
264
  } else {
@@ -261,6 +266,20 @@ export function longform(
261
266
  }
262
267
  }
263
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
+
264
283
  fragment.html += '>';
265
284
 
266
285
  if (Array.isArray(element.chain)) {
@@ -311,7 +330,7 @@ export function longform(
311
330
  fragment.els[fragment.els.length - 1].indent !== targetIndent - 1
312
331
  )
313
332
  ) {
314
- const element = fragment.els.pop();
333
+ const element = fragment.els.pop() as WorkingElement;
315
334
 
316
335
  if (Array.isArray(element.chain)) {
317
336
  for (let i = 0, l = element.chain.length; i < l; i++) {
@@ -347,7 +366,7 @@ export function longform(
347
366
 
348
367
  // If this is a script tag or preformatted block
349
368
  // we want to retain the intended formatting less
350
- // the indent. Preformatting can apply to any element
369
+ // the indent. Pre-formatting can apply to any element
351
370
  // by ending the declaration with `:: {`.
352
371
  if (verbatimIndent != null) {
353
372
  // inside a script or preformatted block
@@ -402,21 +421,45 @@ export function longform(
402
421
 
403
422
  switch (m1[DIRECTIVE]) {
404
423
  case 'id': {
405
- // TODO: Add id validation
406
- id = inlineArgs.trim();
424
+ id = id ?? new URL(inlineArgs.trim(), args?.base).toString();
407
425
  continue main;
408
426
  }
409
427
  case 'lang': {
410
- lang = inlineArgs.trim();
428
+ lang = lang ?? inlineArgs.trim();
411
429
  continue main;
412
430
  }
413
431
  case 'dir': {
414
- dir = inlineArgs.trim();
432
+ dir = dir ?? inlineArgs.trim();
415
433
  continue main;
416
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
+ }
417
450
  }
418
451
 
419
- 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
+ }
420
463
  }
421
464
 
422
465
  switch (m1[DIRECTIVE_KEY] ?? m1[ID_TYPE] ?? m1[ELEMENT] ?? m1[ATTR]) {
@@ -707,6 +750,8 @@ export function longform(
707
750
  const arr = Array.from(parsed.values());
708
751
 
709
752
  function flatten(fragment: WorkingFragment): WorkingFragment {
753
+ if (fragment.refs == null) fragment.refs = [];
754
+
710
755
  // work backwards so we don't change the html string length
711
756
  // for the later replacements
712
757
  for (let j = fragment.refs.length - 1; j >= 0; j--) {
@@ -747,7 +792,7 @@ export function longform(
747
792
  if (i === 0 && root == null) {
748
793
  continue;
749
794
  } else if (i === 0) {
750
- fragment = root;
795
+ fragment = root as WorkingFragment;
751
796
  } else {
752
797
  fragment = arr[i - 1];
753
798
  }
@@ -761,41 +806,44 @@ export function longform(
761
806
 
762
807
  if (root?.html != null) {
763
808
  output.root = root.html;
764
- output.selector = `[data-lf-root]`;
809
+ output.selector = `[data-${key}-root]`;
765
810
  }
766
811
 
767
812
  for (let i = 0; i < arr.length; i++) {
768
- let selector: string;
769
- const fragment = arr[i];
813
+ let selector!: string;
814
+ const fragment: WorkingFragment = arr[i];
770
815
 
771
- if (fragment == null || claimed.has(fragment.id)) {
816
+ if (fragment == null || claimed.has(fragment.id as string)) {
772
817
  continue;
773
818
  }
774
819
 
775
- if (fragment.type === 'embed') {
776
- selector = `[id=${fragment.id}]`;
777
- } else if (fragment.type === 'bare') {
778
- selector = `[data-lf=${fragment.id}]`;
779
- } else if (fragment.type === 'range') {
780
- 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}]`;
781
829
  }
782
830
 
783
- output.fragments[fragment.id] = {
784
- id: fragment.id,
831
+ output.fragments[fragment.id as string] = {
832
+ id: fragment.id as string,
785
833
  selector,
786
834
  type: fragment.type as 'embed' | 'bare' | 'range',
787
835
  html: fragment.html,
788
836
  };
789
837
  }
790
838
 
839
+ output.key = key;
791
840
  output.id = doc?.id;
792
841
  output.lang = doc?.lang;
842
+ output.dir = doc?.dir;
793
843
 
794
844
  return output;
795
845
  }
796
846
 
797
-
798
-
799
847
  /**
800
848
  * Processes a client side Longform template to HTML fragment string.
801
849
  *
@@ -823,3 +871,4 @@ export function processTemplate(
823
871
 
824
872
  return Object.values(longform(lf).fragments)[0]?.html ?? null;
825
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.10",
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": [