unhead 0.1.4 → 0.2.3

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/dist/index.cjs CHANGED
@@ -26,10 +26,11 @@ const TagConfigKeys = ["tagPosition", "tagPriority", "tagDuplicateStrategy"];
26
26
 
27
27
  function normaliseTag(tagName, input) {
28
28
  const tag = { tag: tagName, props: {} };
29
- if (tagName === "title")
30
- tag.children = String(input);
31
- else
32
- tag.props = normaliseProps({ ...input });
29
+ if (tagName === "title" || tagName === "titleTemplate") {
30
+ tag.children = input;
31
+ return tag;
32
+ }
33
+ tag.props = normaliseProps({ ...input });
33
34
  ["children", "innerHtml", "innerHTML"].forEach((key) => {
34
35
  if (typeof tag.props[key] !== "undefined") {
35
36
  tag.children = tag.props[key];
@@ -66,6 +67,146 @@ function normaliseProps(props) {
66
67
  return props;
67
68
  }
68
69
 
70
+ function unpackToArray(input, options) {
71
+ const unpacked = [];
72
+ const kFn = options.resolveKeyData || ((ctx) => ctx.key);
73
+ const vFn = options.resolveValueData || ((ctx) => ctx.value);
74
+ for (const [k, v] of Object.entries(input)) {
75
+ unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
76
+ const ctx = { key: k, value: i };
77
+ const val = vFn(ctx);
78
+ if (typeof val === "object")
79
+ return unpackToArray(val, options);
80
+ if (Array.isArray(val))
81
+ return val;
82
+ return {
83
+ [typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
84
+ [typeof options.value === "function" ? options.value(ctx) : options.value]: val
85
+ };
86
+ }).flat());
87
+ }
88
+ return unpacked;
89
+ }
90
+
91
+ function unpackToString(value, options) {
92
+ return Object.entries(value).map(([key, value2]) => {
93
+ if (typeof value2 === "object")
94
+ value2 = unpackToString(value2, options);
95
+ if (options.resolve) {
96
+ const resolved = options.resolve({ key, value: value2 });
97
+ if (resolved)
98
+ return resolved;
99
+ }
100
+ if (typeof value2 === "number")
101
+ value2 = value2.toString();
102
+ if (typeof value2 === "string" && options.wrapValue) {
103
+ value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
104
+ value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
105
+ }
106
+ return `${key}${options.keyValueSeparator || ""}${value2}`;
107
+ }).join(options.entrySeparator || "");
108
+ }
109
+
110
+ const MetaPackingSchema = {
111
+ robots: {
112
+ unpack: {
113
+ keyValueSeparator: ":"
114
+ }
115
+ },
116
+ contentSecurityPolicy: {
117
+ unpack: {
118
+ keyValueSeparator: " ",
119
+ entrySeparator: "; "
120
+ },
121
+ metaKey: "http-equiv"
122
+ },
123
+ fbAppId: {
124
+ keyValue: "fb:app_id",
125
+ metaKey: "property"
126
+ },
127
+ msapplicationTileImage: {
128
+ keyValue: "msapplication-TileImage"
129
+ },
130
+ msapplicationTileColor: {
131
+ keyValue: "msapplication-TileColor"
132
+ },
133
+ msapplicationConfig: {
134
+ keyValue: "msapplication-Config"
135
+ },
136
+ charset: {
137
+ metaKey: "charset"
138
+ },
139
+ contentType: {
140
+ metaKey: "http-equiv"
141
+ },
142
+ defaultStyle: {
143
+ metaKey: "http-equiv"
144
+ },
145
+ xUaCompatible: {
146
+ metaKey: "http-equiv"
147
+ },
148
+ refresh: {
149
+ metaKey: "http-equiv"
150
+ }
151
+ };
152
+ function resolveMetaKeyType(key) {
153
+ return PropertyPrefixKeys.test(key) ? "property" : MetaPackingSchema[key]?.metaKey || "name";
154
+ }
155
+
156
+ function unpackMeta(input) {
157
+ return unpackToArray(input, {
158
+ key({ key }) {
159
+ return resolveMetaKeyType(key);
160
+ },
161
+ value({ key }) {
162
+ return key === "charset" ? "charset" : "content";
163
+ },
164
+ resolveKeyData({ key }) {
165
+ return MetaPackingSchema[key]?.keyValue || fixKeyCase(key);
166
+ },
167
+ resolveValueData({ value, key }) {
168
+ if (typeof value === "object") {
169
+ const definition = MetaPackingSchema[key];
170
+ if (key === "refresh")
171
+ return `${value.seconds};url=${value.url}`;
172
+ return unpackToString(
173
+ changeKeyCasingDeep(value),
174
+ {
175
+ entrySeparator: ", ",
176
+ keyValueSeparator: "=",
177
+ resolve({ value: value2, key: key2 }) {
178
+ if (typeof value2 === "boolean")
179
+ return `${key2}`;
180
+ },
181
+ ...definition?.unpack
182
+ }
183
+ );
184
+ }
185
+ return typeof value === "number" ? value.toString() : value;
186
+ }
187
+ });
188
+ }
189
+
190
+ const PropertyPrefixKeys = /^(og|twitter|fb)/;
191
+ function fixKeyCase(key) {
192
+ key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
193
+ if (PropertyPrefixKeys.test(key)) {
194
+ key = key.replace("secure-url", "secure_url").replace(/-/g, ":");
195
+ }
196
+ return key;
197
+ }
198
+ function changeKeyCasingDeep(input) {
199
+ if (Array.isArray(input)) {
200
+ return input.map((entry) => changeKeyCasingDeep(entry));
201
+ }
202
+ if (typeof input !== "object" || Array.isArray(input))
203
+ return input;
204
+ const output = {};
205
+ for (const [key, value] of Object.entries(input))
206
+ output[fixKeyCase(key)] = changeKeyCasingDeep(value);
207
+ return output;
208
+ }
209
+
69
210
  const tagWeight = (tag) => {
70
211
  if (typeof tag.tagPriority === "number")
71
212
  return tag.tagPriority;
@@ -125,7 +266,7 @@ function resolveTitleTemplateFromTags(tags) {
125
266
  if (newTitle !== null) {
126
267
  tags[titleIdx].children = newTitle || tags[titleIdx].children;
127
268
  } else {
128
- tags = tags.filter((_, i) => i !== titleIdx);
269
+ delete tags[titleIdx];
129
270
  }
130
271
  } else if (titleTemplateIdx !== -1) {
131
272
  const newTitle = renderTitleTemplate(
@@ -136,9 +277,10 @@ function resolveTitleTemplateFromTags(tags) {
136
277
  tags[titleTemplateIdx].tag = "title";
137
278
  }
138
279
  }
139
- if (titleTemplateIdx !== -1)
140
- tags = tags.filter((_, i) => i !== titleTemplateIdx);
141
- return tags;
280
+ if (titleTemplateIdx !== -1) {
281
+ delete tags[titleTemplateIdx];
282
+ }
283
+ return tags.filter(Boolean);
142
284
  }
143
285
 
144
286
  const DedupesTagsPlugin = (options) => {
@@ -230,6 +372,23 @@ const TitleTemplatePlugin = () => {
230
372
  });
231
373
  };
232
374
 
375
+ function defineHeadPlugin(plugin) {
376
+ return plugin;
377
+ }
378
+
379
+ const DeprecatedTagAttrPlugin = () => {
380
+ return defineHeadPlugin({
381
+ hooks: {
382
+ "tag:normalise": function({ tag }) {
383
+ if (tag.props.body) {
384
+ tag.tagPosition = "bodyClose";
385
+ delete tag.props.body;
386
+ }
387
+ }
388
+ }
389
+ });
390
+ };
391
+
233
392
  function hashCode(s) {
234
393
  let h = 9;
235
394
  for (let i = 0; i < s.length; )
@@ -254,6 +413,10 @@ const HydratesStatePlugin = () => {
254
413
  });
255
414
  };
256
415
 
416
+ function asArray(value) {
417
+ return Array.isArray(value) ? value : [value];
418
+ }
419
+
257
420
  const IsClient = typeof window !== "undefined";
258
421
 
259
422
  exports.activeHead = void 0;
@@ -266,26 +429,29 @@ function useHead(input, options = {}) {
266
429
  const head = getActiveHead();
267
430
  head.push(input, options);
268
431
  }
269
- function useServerHead(input, options = {}) {
270
- useHead(input, { ...options, mode: "server" });
271
- }
272
- const useTitle = (title) => {
432
+ const useTagTitle = (title) => {
273
433
  useHead({ title });
274
434
  };
275
- const useMeta = (meta) => {
276
- useHead({ meta: [meta] });
435
+ const useTagBase = (base) => {
436
+ useHead({ base });
277
437
  };
278
- const useLink = (link) => {
279
- useHead({ link: [link] });
438
+ const useTagMeta = (meta) => {
439
+ useHead({ meta: asArray(meta) });
280
440
  };
281
- const useScript = (script) => {
282
- useHead({ script: [script] });
441
+ const useTagMetaFlat = (meta) => {
442
+ useTagMeta(unpackMeta(meta));
283
443
  };
284
- const useStyle = (style) => {
285
- useHead({ style: [style] });
444
+ const useTagLink = (link) => {
445
+ useHead({ link: asArray(link) });
286
446
  };
287
- const useBase = (base) => {
288
- useHead({ base });
447
+ const useTagScript = (script) => {
448
+ useHead({ script: asArray(script) });
449
+ };
450
+ const useTagStyle = (style) => {
451
+ useHead({ style: asArray(style) });
452
+ };
453
+ const useTagNoscript = (noscript) => {
454
+ useHead({ noscript: asArray(noscript) });
289
455
  };
290
456
  const useHtmlAttrs = (attrs) => {
291
457
  useHead({ htmlAttrs: attrs });
@@ -296,13 +462,43 @@ const useBodyAttrs = (attrs) => {
296
462
  const useTitleTemplate = (titleTemplate) => {
297
463
  useHead({ titleTemplate });
298
464
  };
299
- const useNoscript = (noscript) => {
300
- useHead({ noscript: [noscript] });
301
- };
302
465
 
303
- function asArray(value) {
304
- return Array.isArray(value) ? value : [value];
466
+ function useServerHead(input, options = {}) {
467
+ useHead(input, { ...options, mode: "server" });
305
468
  }
469
+ const useServerTagTitle = (title) => {
470
+ useServerHead({ title });
471
+ };
472
+ const useServerTagBase = (base) => {
473
+ useServerHead({ base });
474
+ };
475
+ const useServerTagMeta = (meta) => {
476
+ useServerHead({ meta: asArray(meta) });
477
+ };
478
+ const useServerTagMetaFlat = (meta) => {
479
+ useServerTagMeta(unpackMeta(meta));
480
+ };
481
+ const useServerTagLink = (link) => {
482
+ useServerHead({ link: asArray(link) });
483
+ };
484
+ const useServerTagScript = (script) => {
485
+ useServerHead({ script: asArray(script) });
486
+ };
487
+ const useServerTagStyle = (style) => {
488
+ useServerHead({ style: asArray(style) });
489
+ };
490
+ const useServerTagNoscript = (noscript) => {
491
+ useServerHead({ noscript: asArray(noscript) });
492
+ };
493
+ const useServerHtmlAttrs = (attrs) => {
494
+ useServerHead({ htmlAttrs: attrs });
495
+ };
496
+ const useServerBodyAttrs = (attrs) => {
497
+ useServerHead({ bodyAttrs: attrs });
498
+ };
499
+ const useServerTitleTemplate = (titleTemplate) => {
500
+ useServerHead({ titleTemplate });
501
+ };
306
502
 
307
503
  function normaliseEntryTags(e) {
308
504
  return Object.entries(e.input).filter(([k, v]) => typeof v !== "undefined" && ValidHeadTags.includes(k)).map(
@@ -314,7 +510,7 @@ function normaliseEntryTags(e) {
314
510
  });
315
511
  }
316
512
 
317
- async function createHead(options = {}) {
513
+ function createHead(options = {}) {
318
514
  let entries = [];
319
515
  let _sde = {};
320
516
  let entryId = 0;
@@ -322,12 +518,14 @@ async function createHead(options = {}) {
322
518
  if (options.hooks)
323
519
  hooks.addHooks(options.hooks);
324
520
  const plugins = [
521
+ DeprecatedTagAttrPlugin(),
325
522
  DedupesTagsPlugin(),
326
523
  SortTagsPlugin(),
327
524
  TitleTemplatePlugin()
328
525
  ];
329
526
  plugins.push(...options.plugins || []);
330
527
  plugins.forEach((plugin) => hooks.addHooks(plugin.hooks || {}));
528
+ const triggerUpdate = () => hooks.callHook("entries:updated", head);
331
529
  const head = {
332
530
  _removeQueuedSideEffect(key) {
333
531
  delete _sde[key];
@@ -350,28 +548,29 @@ async function createHead(options = {}) {
350
548
  _sde: {},
351
549
  ...options2
352
550
  });
353
- hooks.callHook("entries:updated", head);
551
+ triggerUpdate();
552
+ const queueSideEffects = (e) => {
553
+ _sde = { ..._sde, ...e._sde || {} };
554
+ e._sde = {};
555
+ triggerUpdate();
556
+ };
354
557
  return {
355
558
  dispose() {
356
559
  entries = entries.filter((e) => {
357
560
  if (e._i !== _i)
358
561
  return true;
359
- _sde = { ..._sde, ...e._sde || {} };
360
- e._sde = {};
562
+ queueSideEffects(e);
361
563
  return false;
362
564
  });
363
- hooks.callHook("entries:updated", head);
364
565
  },
365
566
  patch(input2) {
366
567
  entries = entries.map((e) => {
367
568
  if (e._i === _i) {
368
- _sde = { ..._sde, ...e._sde || {} };
369
- e._sde = {};
569
+ queueSideEffects(e);
370
570
  e.input = e._i === _i ? input2 : e.input;
371
571
  }
372
572
  return e;
373
573
  });
374
- hooks.callHook("entries:updated", head);
375
574
  }
376
575
  };
377
576
  },
@@ -389,16 +588,13 @@ async function createHead(options = {}) {
389
588
  return resolveCtx.tags;
390
589
  }
391
590
  };
392
- await head.hooks.callHook("init", head);
591
+ head.hooks.callHook("init", head);
393
592
  setActiveHead(head);
394
593
  return head;
395
594
  }
396
595
 
397
- function defineHeadPlugin(plugin) {
398
- return plugin;
399
- }
400
-
401
596
  exports.DedupesTagsPlugin = DedupesTagsPlugin;
597
+ exports.DeprecatedTagAttrPlugin = DeprecatedTagAttrPlugin;
402
598
  exports.HydratesStatePlugin = HydratesStatePlugin;
403
599
  exports.SortTagsPlugin = SortTagsPlugin;
404
600
  exports.TitleTemplatePlugin = TitleTemplatePlugin;
@@ -408,15 +604,27 @@ exports.defineHeadPlugin = defineHeadPlugin;
408
604
  exports.getActiveHead = getActiveHead;
409
605
  exports.normaliseEntryTags = normaliseEntryTags;
410
606
  exports.setActiveHead = setActiveHead;
411
- exports.useBase = useBase;
412
607
  exports.useBodyAttrs = useBodyAttrs;
413
608
  exports.useHead = useHead;
414
609
  exports.useHtmlAttrs = useHtmlAttrs;
415
- exports.useLink = useLink;
416
- exports.useMeta = useMeta;
417
- exports.useNoscript = useNoscript;
418
- exports.useScript = useScript;
610
+ exports.useServerBodyAttrs = useServerBodyAttrs;
419
611
  exports.useServerHead = useServerHead;
420
- exports.useStyle = useStyle;
421
- exports.useTitle = useTitle;
612
+ exports.useServerHtmlAttrs = useServerHtmlAttrs;
613
+ exports.useServerTagBase = useServerTagBase;
614
+ exports.useServerTagLink = useServerTagLink;
615
+ exports.useServerTagMeta = useServerTagMeta;
616
+ exports.useServerTagMetaFlat = useServerTagMetaFlat;
617
+ exports.useServerTagNoscript = useServerTagNoscript;
618
+ exports.useServerTagScript = useServerTagScript;
619
+ exports.useServerTagStyle = useServerTagStyle;
620
+ exports.useServerTagTitle = useServerTagTitle;
621
+ exports.useServerTitleTemplate = useServerTitleTemplate;
622
+ exports.useTagBase = useTagBase;
623
+ exports.useTagLink = useTagLink;
624
+ exports.useTagMeta = useTagMeta;
625
+ exports.useTagMetaFlat = useTagMetaFlat;
626
+ exports.useTagNoscript = useTagNoscript;
627
+ exports.useTagScript = useTagScript;
628
+ exports.useTagStyle = useTagStyle;
629
+ exports.useTagTitle = useTagTitle;
422
630
  exports.useTitleTemplate = useTitleTemplate;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _unhead_schema from '@unhead/schema';
2
- import { Head, HeadEntryOptions, Meta, Link, Script, Style, Base, HtmlAttributes, BodyAttributes, Noscript, HeadClient, CreateHeadOptions, HeadPlugin, HeadEntry, HeadTag } from '@unhead/schema';
2
+ import { Head, HeadEntryOptions, ActiveHeadEntry, Title, Base, Meta, MetaFlatInput, Link, Script, Style, Noscript, HtmlAttributes, BodyAttributes, TitleTemplate, HeadClient, CreateHeadOptions, HeadPlugin, HeadEntry, HeadTag } from '@unhead/schema';
3
3
 
4
4
  interface DedupesTagsPluginOptions {
5
5
  dedupeKeys?: string[];
@@ -10,32 +10,47 @@ declare const SortTagsPlugin: () => _unhead_schema.HeadPlugin;
10
10
 
11
11
  declare const TitleTemplatePlugin: () => _unhead_schema.HeadPlugin;
12
12
 
13
+ declare const DeprecatedTagAttrPlugin: () => _unhead_schema.HeadPlugin;
14
+
13
15
  declare const HydratesStatePlugin: () => _unhead_schema.HeadPlugin;
14
16
 
15
- declare function useHead<T extends Head>(input: T, options?: HeadEntryOptions): void;
16
- declare function useServerHead<T extends Head>(input: T, options?: HeadEntryOptions): void;
17
- declare const useTitle: (title: string) => void;
18
- declare const useMeta: (meta: Meta) => void;
19
- declare const useLink: (link: Link) => void;
20
- declare const useScript: (script: Script) => void;
21
- declare const useStyle: (style: Style) => void;
22
- declare const useBase: (base: Base) => void;
17
+ declare type Arrayable<T> = T | Array<T>;
18
+ declare function asArray<T>(value: Arrayable<T>): T[];
19
+
20
+ declare function useHead<T extends Head>(input: T, options?: HeadEntryOptions): ActiveHeadEntry<T> | void;
21
+ declare const useTagTitle: (title: Title) => void;
22
+ declare const useTagBase: (base: Base) => void;
23
+ declare const useTagMeta: (meta: Arrayable<Meta>) => void;
24
+ declare const useTagMetaFlat: (meta: MetaFlatInput) => void;
25
+ declare const useTagLink: (link: Arrayable<Link>) => void;
26
+ declare const useTagScript: (script: Arrayable<Script>) => void;
27
+ declare const useTagStyle: (style: Arrayable<Style>) => void;
28
+ declare const useTagNoscript: (noscript: Arrayable<Noscript>) => void;
23
29
  declare const useHtmlAttrs: (attrs: HtmlAttributes) => void;
24
30
  declare const useBodyAttrs: (attrs: BodyAttributes) => void;
25
- declare const useTitleTemplate: (titleTemplate: string) => void;
26
- declare const useNoscript: (noscript: Noscript) => void;
31
+ declare const useTitleTemplate: (titleTemplate: TitleTemplate) => void;
32
+
33
+ declare function useServerHead<T extends Head>(input: T, options?: HeadEntryOptions): ActiveHeadEntry<T> | void;
34
+ declare const useServerTagTitle: (title: Title) => void;
35
+ declare const useServerTagBase: (base: Base) => void;
36
+ declare const useServerTagMeta: (meta: Arrayable<Meta>) => void;
37
+ declare const useServerTagMetaFlat: (meta: MetaFlatInput) => void;
38
+ declare const useServerTagLink: (link: Arrayable<Link>) => void;
39
+ declare const useServerTagScript: (script: Arrayable<Script>) => void;
40
+ declare const useServerTagStyle: (style: Arrayable<Style>) => void;
41
+ declare const useServerTagNoscript: (noscript: Arrayable<Noscript>) => void;
42
+ declare const useServerHtmlAttrs: (attrs: HtmlAttributes) => void;
43
+ declare const useServerBodyAttrs: (attrs: BodyAttributes) => void;
44
+ declare const useServerTitleTemplate: (titleTemplate: TitleTemplate) => void;
27
45
 
28
46
  declare let activeHead: HeadClient<any> | undefined;
29
47
  declare const setActiveHead: <T extends HeadClient<_unhead_schema.Head<_unhead_schema.SchemaAugmentations>>>(head: T | undefined) => T | undefined;
30
48
  declare const getActiveHead: <T extends HeadClient<_unhead_schema.Head<_unhead_schema.SchemaAugmentations>>>() => T;
31
49
 
32
- declare function createHead<T extends {} = Head>(options?: CreateHeadOptions): Promise<HeadClient<T>>;
50
+ declare function createHead<T extends {} = Head>(options?: CreateHeadOptions): HeadClient<T>;
33
51
 
34
52
  declare function defineHeadPlugin(plugin: HeadPlugin): HeadPlugin;
35
53
 
36
54
  declare function normaliseEntryTags<T extends {} = Head>(e: HeadEntry<T>): HeadTag[];
37
55
 
38
- declare type Arrayable<T> = T | Array<T>;
39
- declare function asArray<T>(value: Arrayable<T>): T[];
40
-
41
- export { Arrayable, DedupesTagsPlugin, DedupesTagsPluginOptions, HydratesStatePlugin, SortTagsPlugin, TitleTemplatePlugin, activeHead, asArray, createHead, defineHeadPlugin, getActiveHead, normaliseEntryTags, setActiveHead, useBase, useBodyAttrs, useHead, useHtmlAttrs, useLink, useMeta, useNoscript, useScript, useServerHead, useStyle, useTitle, useTitleTemplate };
56
+ export { Arrayable, DedupesTagsPlugin, DedupesTagsPluginOptions, DeprecatedTagAttrPlugin, HydratesStatePlugin, SortTagsPlugin, TitleTemplatePlugin, activeHead, asArray, createHead, defineHeadPlugin, getActiveHead, normaliseEntryTags, setActiveHead, useBodyAttrs, useHead, useHtmlAttrs, useServerBodyAttrs, useServerHead, useServerHtmlAttrs, useServerTagBase, useServerTagLink, useServerTagMeta, useServerTagMetaFlat, useServerTagNoscript, useServerTagScript, useServerTagStyle, useServerTagTitle, useServerTitleTemplate, useTagBase, useTagLink, useTagMeta, useTagMetaFlat, useTagNoscript, useTagScript, useTagStyle, useTagTitle, useTitleTemplate };
package/dist/index.mjs CHANGED
@@ -24,10 +24,11 @@ const TagConfigKeys = ["tagPosition", "tagPriority", "tagDuplicateStrategy"];
24
24
 
25
25
  function normaliseTag(tagName, input) {
26
26
  const tag = { tag: tagName, props: {} };
27
- if (tagName === "title")
28
- tag.children = String(input);
29
- else
30
- tag.props = normaliseProps({ ...input });
27
+ if (tagName === "title" || tagName === "titleTemplate") {
28
+ tag.children = input;
29
+ return tag;
30
+ }
31
+ tag.props = normaliseProps({ ...input });
31
32
  ["children", "innerHtml", "innerHTML"].forEach((key) => {
32
33
  if (typeof tag.props[key] !== "undefined") {
33
34
  tag.children = tag.props[key];
@@ -64,6 +65,146 @@ function normaliseProps(props) {
64
65
  return props;
65
66
  }
66
67
 
68
+ function unpackToArray(input, options) {
69
+ const unpacked = [];
70
+ const kFn = options.resolveKeyData || ((ctx) => ctx.key);
71
+ const vFn = options.resolveValueData || ((ctx) => ctx.value);
72
+ for (const [k, v] of Object.entries(input)) {
73
+ unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
74
+ const ctx = { key: k, value: i };
75
+ const val = vFn(ctx);
76
+ if (typeof val === "object")
77
+ return unpackToArray(val, options);
78
+ if (Array.isArray(val))
79
+ return val;
80
+ return {
81
+ [typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
82
+ [typeof options.value === "function" ? options.value(ctx) : options.value]: val
83
+ };
84
+ }).flat());
85
+ }
86
+ return unpacked;
87
+ }
88
+
89
+ function unpackToString(value, options) {
90
+ return Object.entries(value).map(([key, value2]) => {
91
+ if (typeof value2 === "object")
92
+ value2 = unpackToString(value2, options);
93
+ if (options.resolve) {
94
+ const resolved = options.resolve({ key, value: value2 });
95
+ if (resolved)
96
+ return resolved;
97
+ }
98
+ if (typeof value2 === "number")
99
+ value2 = value2.toString();
100
+ if (typeof value2 === "string" && options.wrapValue) {
101
+ value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
102
+ value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
103
+ }
104
+ return `${key}${options.keyValueSeparator || ""}${value2}`;
105
+ }).join(options.entrySeparator || "");
106
+ }
107
+
108
+ const MetaPackingSchema = {
109
+ robots: {
110
+ unpack: {
111
+ keyValueSeparator: ":"
112
+ }
113
+ },
114
+ contentSecurityPolicy: {
115
+ unpack: {
116
+ keyValueSeparator: " ",
117
+ entrySeparator: "; "
118
+ },
119
+ metaKey: "http-equiv"
120
+ },
121
+ fbAppId: {
122
+ keyValue: "fb:app_id",
123
+ metaKey: "property"
124
+ },
125
+ msapplicationTileImage: {
126
+ keyValue: "msapplication-TileImage"
127
+ },
128
+ msapplicationTileColor: {
129
+ keyValue: "msapplication-TileColor"
130
+ },
131
+ msapplicationConfig: {
132
+ keyValue: "msapplication-Config"
133
+ },
134
+ charset: {
135
+ metaKey: "charset"
136
+ },
137
+ contentType: {
138
+ metaKey: "http-equiv"
139
+ },
140
+ defaultStyle: {
141
+ metaKey: "http-equiv"
142
+ },
143
+ xUaCompatible: {
144
+ metaKey: "http-equiv"
145
+ },
146
+ refresh: {
147
+ metaKey: "http-equiv"
148
+ }
149
+ };
150
+ function resolveMetaKeyType(key) {
151
+ return PropertyPrefixKeys.test(key) ? "property" : MetaPackingSchema[key]?.metaKey || "name";
152
+ }
153
+
154
+ function unpackMeta(input) {
155
+ return unpackToArray(input, {
156
+ key({ key }) {
157
+ return resolveMetaKeyType(key);
158
+ },
159
+ value({ key }) {
160
+ return key === "charset" ? "charset" : "content";
161
+ },
162
+ resolveKeyData({ key }) {
163
+ return MetaPackingSchema[key]?.keyValue || fixKeyCase(key);
164
+ },
165
+ resolveValueData({ value, key }) {
166
+ if (typeof value === "object") {
167
+ const definition = MetaPackingSchema[key];
168
+ if (key === "refresh")
169
+ return `${value.seconds};url=${value.url}`;
170
+ return unpackToString(
171
+ changeKeyCasingDeep(value),
172
+ {
173
+ entrySeparator: ", ",
174
+ keyValueSeparator: "=",
175
+ resolve({ value: value2, key: key2 }) {
176
+ if (typeof value2 === "boolean")
177
+ return `${key2}`;
178
+ },
179
+ ...definition?.unpack
180
+ }
181
+ );
182
+ }
183
+ return typeof value === "number" ? value.toString() : value;
184
+ }
185
+ });
186
+ }
187
+
188
+ const PropertyPrefixKeys = /^(og|twitter|fb)/;
189
+ function fixKeyCase(key) {
190
+ key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
191
+ if (PropertyPrefixKeys.test(key)) {
192
+ key = key.replace("secure-url", "secure_url").replace(/-/g, ":");
193
+ }
194
+ return key;
195
+ }
196
+ function changeKeyCasingDeep(input) {
197
+ if (Array.isArray(input)) {
198
+ return input.map((entry) => changeKeyCasingDeep(entry));
199
+ }
200
+ if (typeof input !== "object" || Array.isArray(input))
201
+ return input;
202
+ const output = {};
203
+ for (const [key, value] of Object.entries(input))
204
+ output[fixKeyCase(key)] = changeKeyCasingDeep(value);
205
+ return output;
206
+ }
207
+
67
208
  const tagWeight = (tag) => {
68
209
  if (typeof tag.tagPriority === "number")
69
210
  return tag.tagPriority;
@@ -123,7 +264,7 @@ function resolveTitleTemplateFromTags(tags) {
123
264
  if (newTitle !== null) {
124
265
  tags[titleIdx].children = newTitle || tags[titleIdx].children;
125
266
  } else {
126
- tags = tags.filter((_, i) => i !== titleIdx);
267
+ delete tags[titleIdx];
127
268
  }
128
269
  } else if (titleTemplateIdx !== -1) {
129
270
  const newTitle = renderTitleTemplate(
@@ -134,9 +275,10 @@ function resolveTitleTemplateFromTags(tags) {
134
275
  tags[titleTemplateIdx].tag = "title";
135
276
  }
136
277
  }
137
- if (titleTemplateIdx !== -1)
138
- tags = tags.filter((_, i) => i !== titleTemplateIdx);
139
- return tags;
278
+ if (titleTemplateIdx !== -1) {
279
+ delete tags[titleTemplateIdx];
280
+ }
281
+ return tags.filter(Boolean);
140
282
  }
141
283
 
142
284
  const DedupesTagsPlugin = (options) => {
@@ -228,6 +370,23 @@ const TitleTemplatePlugin = () => {
228
370
  });
229
371
  };
230
372
 
373
+ function defineHeadPlugin(plugin) {
374
+ return plugin;
375
+ }
376
+
377
+ const DeprecatedTagAttrPlugin = () => {
378
+ return defineHeadPlugin({
379
+ hooks: {
380
+ "tag:normalise": function({ tag }) {
381
+ if (tag.props.body) {
382
+ tag.tagPosition = "bodyClose";
383
+ delete tag.props.body;
384
+ }
385
+ }
386
+ }
387
+ });
388
+ };
389
+
231
390
  function hashCode(s) {
232
391
  let h = 9;
233
392
  for (let i = 0; i < s.length; )
@@ -252,6 +411,10 @@ const HydratesStatePlugin = () => {
252
411
  });
253
412
  };
254
413
 
414
+ function asArray(value) {
415
+ return Array.isArray(value) ? value : [value];
416
+ }
417
+
255
418
  const IsClient = typeof window !== "undefined";
256
419
 
257
420
  let activeHead;
@@ -264,26 +427,29 @@ function useHead(input, options = {}) {
264
427
  const head = getActiveHead();
265
428
  head.push(input, options);
266
429
  }
267
- function useServerHead(input, options = {}) {
268
- useHead(input, { ...options, mode: "server" });
269
- }
270
- const useTitle = (title) => {
430
+ const useTagTitle = (title) => {
271
431
  useHead({ title });
272
432
  };
273
- const useMeta = (meta) => {
274
- useHead({ meta: [meta] });
433
+ const useTagBase = (base) => {
434
+ useHead({ base });
275
435
  };
276
- const useLink = (link) => {
277
- useHead({ link: [link] });
436
+ const useTagMeta = (meta) => {
437
+ useHead({ meta: asArray(meta) });
278
438
  };
279
- const useScript = (script) => {
280
- useHead({ script: [script] });
439
+ const useTagMetaFlat = (meta) => {
440
+ useTagMeta(unpackMeta(meta));
281
441
  };
282
- const useStyle = (style) => {
283
- useHead({ style: [style] });
442
+ const useTagLink = (link) => {
443
+ useHead({ link: asArray(link) });
284
444
  };
285
- const useBase = (base) => {
286
- useHead({ base });
445
+ const useTagScript = (script) => {
446
+ useHead({ script: asArray(script) });
447
+ };
448
+ const useTagStyle = (style) => {
449
+ useHead({ style: asArray(style) });
450
+ };
451
+ const useTagNoscript = (noscript) => {
452
+ useHead({ noscript: asArray(noscript) });
287
453
  };
288
454
  const useHtmlAttrs = (attrs) => {
289
455
  useHead({ htmlAttrs: attrs });
@@ -294,13 +460,43 @@ const useBodyAttrs = (attrs) => {
294
460
  const useTitleTemplate = (titleTemplate) => {
295
461
  useHead({ titleTemplate });
296
462
  };
297
- const useNoscript = (noscript) => {
298
- useHead({ noscript: [noscript] });
299
- };
300
463
 
301
- function asArray(value) {
302
- return Array.isArray(value) ? value : [value];
464
+ function useServerHead(input, options = {}) {
465
+ useHead(input, { ...options, mode: "server" });
303
466
  }
467
+ const useServerTagTitle = (title) => {
468
+ useServerHead({ title });
469
+ };
470
+ const useServerTagBase = (base) => {
471
+ useServerHead({ base });
472
+ };
473
+ const useServerTagMeta = (meta) => {
474
+ useServerHead({ meta: asArray(meta) });
475
+ };
476
+ const useServerTagMetaFlat = (meta) => {
477
+ useServerTagMeta(unpackMeta(meta));
478
+ };
479
+ const useServerTagLink = (link) => {
480
+ useServerHead({ link: asArray(link) });
481
+ };
482
+ const useServerTagScript = (script) => {
483
+ useServerHead({ script: asArray(script) });
484
+ };
485
+ const useServerTagStyle = (style) => {
486
+ useServerHead({ style: asArray(style) });
487
+ };
488
+ const useServerTagNoscript = (noscript) => {
489
+ useServerHead({ noscript: asArray(noscript) });
490
+ };
491
+ const useServerHtmlAttrs = (attrs) => {
492
+ useServerHead({ htmlAttrs: attrs });
493
+ };
494
+ const useServerBodyAttrs = (attrs) => {
495
+ useServerHead({ bodyAttrs: attrs });
496
+ };
497
+ const useServerTitleTemplate = (titleTemplate) => {
498
+ useServerHead({ titleTemplate });
499
+ };
304
500
 
305
501
  function normaliseEntryTags(e) {
306
502
  return Object.entries(e.input).filter(([k, v]) => typeof v !== "undefined" && ValidHeadTags.includes(k)).map(
@@ -312,7 +508,7 @@ function normaliseEntryTags(e) {
312
508
  });
313
509
  }
314
510
 
315
- async function createHead(options = {}) {
511
+ function createHead(options = {}) {
316
512
  let entries = [];
317
513
  let _sde = {};
318
514
  let entryId = 0;
@@ -320,12 +516,14 @@ async function createHead(options = {}) {
320
516
  if (options.hooks)
321
517
  hooks.addHooks(options.hooks);
322
518
  const plugins = [
519
+ DeprecatedTagAttrPlugin(),
323
520
  DedupesTagsPlugin(),
324
521
  SortTagsPlugin(),
325
522
  TitleTemplatePlugin()
326
523
  ];
327
524
  plugins.push(...options.plugins || []);
328
525
  plugins.forEach((plugin) => hooks.addHooks(plugin.hooks || {}));
526
+ const triggerUpdate = () => hooks.callHook("entries:updated", head);
329
527
  const head = {
330
528
  _removeQueuedSideEffect(key) {
331
529
  delete _sde[key];
@@ -348,28 +546,29 @@ async function createHead(options = {}) {
348
546
  _sde: {},
349
547
  ...options2
350
548
  });
351
- hooks.callHook("entries:updated", head);
549
+ triggerUpdate();
550
+ const queueSideEffects = (e) => {
551
+ _sde = { ..._sde, ...e._sde || {} };
552
+ e._sde = {};
553
+ triggerUpdate();
554
+ };
352
555
  return {
353
556
  dispose() {
354
557
  entries = entries.filter((e) => {
355
558
  if (e._i !== _i)
356
559
  return true;
357
- _sde = { ..._sde, ...e._sde || {} };
358
- e._sde = {};
560
+ queueSideEffects(e);
359
561
  return false;
360
562
  });
361
- hooks.callHook("entries:updated", head);
362
563
  },
363
564
  patch(input2) {
364
565
  entries = entries.map((e) => {
365
566
  if (e._i === _i) {
366
- _sde = { ..._sde, ...e._sde || {} };
367
- e._sde = {};
567
+ queueSideEffects(e);
368
568
  e.input = e._i === _i ? input2 : e.input;
369
569
  }
370
570
  return e;
371
571
  });
372
- hooks.callHook("entries:updated", head);
373
572
  }
374
573
  };
375
574
  },
@@ -387,13 +586,9 @@ async function createHead(options = {}) {
387
586
  return resolveCtx.tags;
388
587
  }
389
588
  };
390
- await head.hooks.callHook("init", head);
589
+ head.hooks.callHook("init", head);
391
590
  setActiveHead(head);
392
591
  return head;
393
592
  }
394
593
 
395
- function defineHeadPlugin(plugin) {
396
- return plugin;
397
- }
398
-
399
- export { DedupesTagsPlugin, HydratesStatePlugin, SortTagsPlugin, TitleTemplatePlugin, activeHead, asArray, createHead, defineHeadPlugin, getActiveHead, normaliseEntryTags, setActiveHead, useBase, useBodyAttrs, useHead, useHtmlAttrs, useLink, useMeta, useNoscript, useScript, useServerHead, useStyle, useTitle, useTitleTemplate };
594
+ export { DedupesTagsPlugin, DeprecatedTagAttrPlugin, HydratesStatePlugin, SortTagsPlugin, TitleTemplatePlugin, activeHead, asArray, createHead, defineHeadPlugin, getActiveHead, normaliseEntryTags, setActiveHead, useBodyAttrs, useHead, useHtmlAttrs, useServerBodyAttrs, useServerHead, useServerHtmlAttrs, useServerTagBase, useServerTagLink, useServerTagMeta, useServerTagMetaFlat, useServerTagNoscript, useServerTagScript, useServerTagStyle, useServerTagTitle, useServerTitleTemplate, useTagBase, useTagLink, useTagMeta, useTagMetaFlat, useTagNoscript, useTagScript, useTagStyle, useTagTitle, useTitleTemplate };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "unhead",
3
3
  "type": "module",
4
- "version": "0.1.4",
4
+ "version": "0.2.3",
5
5
  "packageManager": "pnpm@7.14.0",
6
6
  "author": "Harlan Wilton <harlan@harlanzw.com>",
7
7
  "license": "MIT",
@@ -30,11 +30,11 @@
30
30
  "dist"
31
31
  ],
32
32
  "dependencies": {
33
- "@unhead/schema": "0.1.4",
33
+ "@unhead/schema": "0.2.3",
34
34
  "hookable": "^5.4.1"
35
35
  },
36
36
  "devDependencies": {
37
- "zhead": "1.0.0-beta.5"
37
+ "zhead": "1.0.0-beta.10"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "unbuild .",