@symbo.ls/smbls-utils 3.5.1 → 3.6.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.
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var cdn_exports = {};
20
+ __export(cdn_exports, {
21
+ CDN_PROVIDERS: () => CDN_PROVIDERS,
22
+ PACKAGE_MANAGER_TO_CDN: () => PACKAGE_MANAGER_TO_CDN,
23
+ getCDNUrl: () => getCDNUrl,
24
+ getCdnProviderFromConfig: () => getCdnProviderFromConfig,
25
+ getImportMapScript: () => getImportMapScript
26
+ });
27
+ module.exports = __toCommonJS(cdn_exports);
28
+ function onlyDotsAndNumbers(str) {
29
+ return /^[0-9.]+$/.test(str) && str !== "";
30
+ }
31
+ const CDN_PROVIDERS = {
32
+ skypack: {
33
+ url: "https://cdn.skypack.dev",
34
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.skypack.url}/${pkg}${version !== "latest" ? `@${version}` : ""}`
35
+ },
36
+ esmsh: {
37
+ url: "https://esm.sh",
38
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.esmsh.url}/${pkg}${version !== "latest" ? `@${version}` : ""}`
39
+ },
40
+ unpkg: {
41
+ url: "https://unpkg.com",
42
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.unpkg.url}/${pkg}${version !== "latest" ? `@${version}` : ""}?module`
43
+ },
44
+ jsdelivr: {
45
+ url: "https://cdn.jsdelivr.net/npm",
46
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.jsdelivr.url}/${pkg}${version !== "latest" ? `@${version}` : ""}/+esm`
47
+ },
48
+ symbols: {
49
+ url: "https://pkg.symbo.ls",
50
+ formatUrl: (pkg, version) => {
51
+ if (pkg.split("/").length > 2 || !onlyDotsAndNumbers(version)) {
52
+ return `${CDN_PROVIDERS.symbols.url}/${pkg}`;
53
+ }
54
+ return `${CDN_PROVIDERS.symbols.url}/${pkg}/${version}.js`;
55
+ }
56
+ }
57
+ };
58
+ const PACKAGE_MANAGER_TO_CDN = {
59
+ "esm.sh": "esmsh",
60
+ "unpkg": "unpkg",
61
+ "skypack": "skypack",
62
+ "jsdelivr": "jsdelivr",
63
+ "pkg.symbo.ls": "symbols"
64
+ };
65
+ const getCdnProviderFromConfig = (symbolsConfig = {}) => {
66
+ const { packageManager } = symbolsConfig;
67
+ return PACKAGE_MANAGER_TO_CDN[packageManager] || null;
68
+ };
69
+ const getCDNUrl = (packageName, version = "latest", provider = "esmsh") => {
70
+ const cdnConfig = CDN_PROVIDERS[provider] || CDN_PROVIDERS.esmsh;
71
+ return cdnConfig.formatUrl(packageName, version);
72
+ };
73
+ const getImportMapScript = (data, defaultProvider = "skypack") => {
74
+ const dependencies = data.dependencies || {};
75
+ const keys = Object.keys(dependencies);
76
+ if (!keys.length) return "";
77
+ const imports = {};
78
+ for (const pkgName of keys) {
79
+ const version = dependencies[pkgName] || "latest";
80
+ imports[pkgName] = getCDNUrl(pkgName, version, defaultProvider);
81
+ }
82
+ return `<script type="importmap">{
83
+ "imports": ${JSON.stringify(imports, null, 2)}
84
+ }<\/script>`;
85
+ };
package/dist/cjs/index.js CHANGED
@@ -36,6 +36,8 @@ __reExport(index_exports, require("./date.js"), module.exports);
36
36
  __reExport(index_exports, require("./fibonacci.js"), module.exports);
37
37
  __reExport(index_exports, require("./load.js"), module.exports);
38
38
  __reExport(index_exports, require("./files.js"), module.exports);
39
+ __reExport(index_exports, require("./cdn.js"), module.exports);
40
+ __reExport(index_exports, require("./metadata.js"), module.exports);
39
41
  const copyStringToClipboard = async (str) => {
40
42
  try {
41
43
  await navigator.clipboard.writeText(str);
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var metadata_exports = {};
20
+ __export(metadata_exports, {
21
+ generateMetaTags: () => generateMetaTags,
22
+ getPageMetadata: () => getPageMetadata
23
+ });
24
+ module.exports = __toCommonJS(metadata_exports);
25
+ var import_utils = require("@domql/utils");
26
+ const escapeHtml = (text) => {
27
+ if (text === null || text === void 0) return "";
28
+ const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
29
+ return text.toString().replace(/[&<>"']/g, (m) => map[m]);
30
+ };
31
+ const buildAttrs = (obj) => Object.entries(obj || {}).filter(([_, v]) => v !== void 0).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(" ");
32
+ const generateMetaTags = (metadata, isProduction) => {
33
+ if (!isProduction) {
34
+ const faviconTag = (() => {
35
+ const fv = metadata?.favicon || metadata?.favicons;
36
+ if (!fv) return '<link rel="icon" href="/favicon.ico">';
37
+ if (typeof fv === "string") return `<link rel="icon" href="${escapeHtml(fv)}">`;
38
+ if (Array.isArray(fv)) {
39
+ return fv.map(
40
+ (item) => typeof item === "string" ? `<link rel="icon" href="${escapeHtml(item)}">` : `<link ${buildAttrs({ rel: "icon", ...item })}>`
41
+ ).join("\n");
42
+ }
43
+ return `<link ${buildAttrs({ rel: "icon", ...fv })}>`;
44
+ })();
45
+ return [
46
+ '<meta charset="UTF-8">',
47
+ `<title>${escapeHtml(metadata.title || "Test")}</title>`,
48
+ '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">',
49
+ '<meta name="robots" content="noindex">',
50
+ '<meta name="apple-mobile-web-app-capable" content="yes">',
51
+ faviconTag
52
+ ].join("\n");
53
+ }
54
+ const tags = Object.entries(metadata).reduce(
55
+ (acc, [key, value]) => {
56
+ if (!value && value !== 0 && value !== false) return acc;
57
+ if (key === "title") {
58
+ acc.push(`<title>${escapeHtml(value)}</title>`);
59
+ return acc;
60
+ }
61
+ if (key === "canonical") {
62
+ acc.push(`<link rel="canonical" href="${escapeHtml(value)}">`);
63
+ return acc;
64
+ }
65
+ if (key === "manifest") {
66
+ acc.push(`<link rel="manifest" href="${escapeHtml(value)}">`);
67
+ return acc;
68
+ }
69
+ if (key === "favicon" || key === "favicons" || key === "icon" || key === "icons") {
70
+ const items = Array.isArray(value) ? value : [value];
71
+ items.forEach((item) => {
72
+ if (typeof item === "string") {
73
+ acc.push(`<link rel="icon" href="${escapeHtml(item)}">`);
74
+ } else if (typeof item === "object") {
75
+ const attrs = buildAttrs(item);
76
+ if (!/rel=/.test(attrs)) {
77
+ acc.push(`<link rel="icon" ${attrs}>`);
78
+ } else {
79
+ acc.push(`<link ${attrs}>`);
80
+ }
81
+ }
82
+ });
83
+ return acc;
84
+ }
85
+ if (key === "alternate") {
86
+ const alternates = Array.isArray(value) ? value : [value];
87
+ alternates.forEach((alt) => {
88
+ if (typeof alt === "object") {
89
+ const attrs = Object.entries(alt).filter(([_, attrValue]) => attrValue !== void 0).map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`).join(" ");
90
+ acc.push(`<link rel="alternate" ${attrs}>`);
91
+ }
92
+ });
93
+ return acc;
94
+ }
95
+ const processMetaTag = (tagKey, tagValue, attrType = "name") => {
96
+ if (typeof tagValue === "string" || typeof tagValue === "number" || typeof tagValue === "boolean") {
97
+ acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(tagValue)}">`);
98
+ } else if (Array.isArray(tagValue)) {
99
+ tagValue.forEach((item) => {
100
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
101
+ acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(item)}">`);
102
+ } else if (typeof item === "object") {
103
+ const attrs = Object.entries(item).filter(([_, attrValue]) => attrValue !== void 0).map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`).join(" ");
104
+ acc.push(`<meta ${attrs}>`);
105
+ }
106
+ });
107
+ } else if (typeof tagValue === "object") {
108
+ const attrs = Object.entries(tagValue).filter(([_, attrValue]) => attrValue !== void 0).map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`).join(" ");
109
+ acc.push(`<meta ${attrs}>`);
110
+ }
111
+ };
112
+ if (key.startsWith("http-equiv:")) {
113
+ const httpEquivKey = key.replace("http-equiv:", "");
114
+ processMetaTag(httpEquivKey, value, "http-equiv");
115
+ return acc;
116
+ }
117
+ if (key.startsWith("itemprop:")) {
118
+ const itempropKey = key.replace("itemprop:", "");
119
+ processMetaTag(itempropKey, value, "itemprop");
120
+ return acc;
121
+ }
122
+ const prefixes = [
123
+ "og:",
124
+ "twitter:",
125
+ "fb:",
126
+ "article:",
127
+ "profile:",
128
+ "book:",
129
+ "business:",
130
+ "music:",
131
+ "product:",
132
+ "video:",
133
+ "DC:",
134
+ "DCTERMS:"
135
+ ];
136
+ const prefix = prefixes.find((p) => key.startsWith(p));
137
+ if (prefix) {
138
+ const tagKey = key.replace(prefix, "");
139
+ processMetaTag(
140
+ `${prefix.replace(":", "")}:${tagKey}`,
141
+ value,
142
+ prefix === "twitter:" || prefix === "DC:" || prefix === "DCTERMS:" ? "name" : "property"
143
+ );
144
+ return acc;
145
+ }
146
+ if (key.startsWith("apple:") || key.startsWith("msapplication:")) {
147
+ const pfx = key.startsWith("apple:") ? "apple-" : "msapplication-";
148
+ const tagKey = key.replace(/^apple:|^msapplication:/, "");
149
+ processMetaTag(`${pfx}${tagKey}`, value, "name");
150
+ return acc;
151
+ }
152
+ processMetaTag(key, value);
153
+ return acc;
154
+ },
155
+ [
156
+ '<meta charset="UTF-8">',
157
+ '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">'
158
+ ]
159
+ );
160
+ return tags.join("\n");
161
+ };
162
+ const isBareFilename = (val) => typeof val === "string" && val.length > 0 && !val.startsWith("/") && !val.startsWith("http");
163
+ function resolveFileReferences(metadata, files) {
164
+ if (!files || typeof files !== "object") return metadata;
165
+ const resolve = (val) => {
166
+ if (!isBareFilename(val)) return val;
167
+ const fileEntry = files[val];
168
+ if (fileEntry?.src) return fileEntry.src;
169
+ return val;
170
+ };
171
+ const result = { ...metadata };
172
+ for (const [key, value] of Object.entries(result)) {
173
+ if (typeof value === "string") {
174
+ result[key] = resolve(value);
175
+ } else if (Array.isArray(value)) {
176
+ result[key] = value.map(
177
+ (item) => typeof item === "string" ? resolve(item) : item
178
+ );
179
+ }
180
+ }
181
+ return result;
182
+ }
183
+ function getPageMetadata(data, pathname) {
184
+ const currentPage = data.pages?.[pathname];
185
+ const stateObject = (0, import_utils.isObject)(currentPage?.state) && currentPage?.state;
186
+ let pageMetadata = currentPage?.metadata || currentPage?.helmet || stateObject || {};
187
+ const appMetadata = data.app?.metadata || {};
188
+ if (data.integrations?.seo) {
189
+ pageMetadata = { ...data.integrations.seo, ...appMetadata, ...pageMetadata };
190
+ } else if (Object.keys(appMetadata).length) {
191
+ pageMetadata = { ...appMetadata, ...pageMetadata };
192
+ }
193
+ if (!pageMetadata.title) pageMetadata.title = data.name + " / symbo.ls" || "Symbols demo";
194
+ pageMetadata = resolveFileReferences(pageMetadata, data.files);
195
+ return pageMetadata;
196
+ }
@@ -0,0 +1,65 @@
1
+ function onlyDotsAndNumbers(str) {
2
+ return /^[0-9.]+$/.test(str) && str !== "";
3
+ }
4
+ const CDN_PROVIDERS = {
5
+ skypack: {
6
+ url: "https://cdn.skypack.dev",
7
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.skypack.url}/${pkg}${version !== "latest" ? `@${version}` : ""}`
8
+ },
9
+ esmsh: {
10
+ url: "https://esm.sh",
11
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.esmsh.url}/${pkg}${version !== "latest" ? `@${version}` : ""}`
12
+ },
13
+ unpkg: {
14
+ url: "https://unpkg.com",
15
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.unpkg.url}/${pkg}${version !== "latest" ? `@${version}` : ""}?module`
16
+ },
17
+ jsdelivr: {
18
+ url: "https://cdn.jsdelivr.net/npm",
19
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.jsdelivr.url}/${pkg}${version !== "latest" ? `@${version}` : ""}/+esm`
20
+ },
21
+ symbols: {
22
+ url: "https://pkg.symbo.ls",
23
+ formatUrl: (pkg, version) => {
24
+ if (pkg.split("/").length > 2 || !onlyDotsAndNumbers(version)) {
25
+ return `${CDN_PROVIDERS.symbols.url}/${pkg}`;
26
+ }
27
+ return `${CDN_PROVIDERS.symbols.url}/${pkg}/${version}.js`;
28
+ }
29
+ }
30
+ };
31
+ const PACKAGE_MANAGER_TO_CDN = {
32
+ "esm.sh": "esmsh",
33
+ "unpkg": "unpkg",
34
+ "skypack": "skypack",
35
+ "jsdelivr": "jsdelivr",
36
+ "pkg.symbo.ls": "symbols"
37
+ };
38
+ const getCdnProviderFromConfig = (symbolsConfig = {}) => {
39
+ const { packageManager } = symbolsConfig;
40
+ return PACKAGE_MANAGER_TO_CDN[packageManager] || null;
41
+ };
42
+ const getCDNUrl = (packageName, version = "latest", provider = "esmsh") => {
43
+ const cdnConfig = CDN_PROVIDERS[provider] || CDN_PROVIDERS.esmsh;
44
+ return cdnConfig.formatUrl(packageName, version);
45
+ };
46
+ const getImportMapScript = (data, defaultProvider = "skypack") => {
47
+ const dependencies = data.dependencies || {};
48
+ const keys = Object.keys(dependencies);
49
+ if (!keys.length) return "";
50
+ const imports = {};
51
+ for (const pkgName of keys) {
52
+ const version = dependencies[pkgName] || "latest";
53
+ imports[pkgName] = getCDNUrl(pkgName, version, defaultProvider);
54
+ }
55
+ return `<script type="importmap">{
56
+ "imports": ${JSON.stringify(imports, null, 2)}
57
+ }<\/script>`;
58
+ };
59
+ export {
60
+ CDN_PROVIDERS,
61
+ PACKAGE_MANAGER_TO_CDN,
62
+ getCDNUrl,
63
+ getCdnProviderFromConfig,
64
+ getImportMapScript
65
+ };
package/dist/esm/index.js CHANGED
@@ -5,6 +5,8 @@ export * from "./date.js";
5
5
  export * from "./fibonacci.js";
6
6
  export * from "./load.js";
7
7
  export * from "./files.js";
8
+ export * from "./cdn.js";
9
+ export * from "./metadata.js";
8
10
  const copyStringToClipboard = async (str) => {
9
11
  try {
10
12
  await navigator.clipboard.writeText(str);
@@ -0,0 +1,176 @@
1
+ import { isObject } from "@domql/utils";
2
+ const escapeHtml = (text) => {
3
+ if (text === null || text === void 0) return "";
4
+ const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
5
+ return text.toString().replace(/[&<>"']/g, (m) => map[m]);
6
+ };
7
+ const buildAttrs = (obj) => Object.entries(obj || {}).filter(([_, v]) => v !== void 0).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(" ");
8
+ const generateMetaTags = (metadata, isProduction) => {
9
+ if (!isProduction) {
10
+ const faviconTag = (() => {
11
+ const fv = metadata?.favicon || metadata?.favicons;
12
+ if (!fv) return '<link rel="icon" href="/favicon.ico">';
13
+ if (typeof fv === "string") return `<link rel="icon" href="${escapeHtml(fv)}">`;
14
+ if (Array.isArray(fv)) {
15
+ return fv.map(
16
+ (item) => typeof item === "string" ? `<link rel="icon" href="${escapeHtml(item)}">` : `<link ${buildAttrs({ rel: "icon", ...item })}>`
17
+ ).join("\n");
18
+ }
19
+ return `<link ${buildAttrs({ rel: "icon", ...fv })}>`;
20
+ })();
21
+ return [
22
+ '<meta charset="UTF-8">',
23
+ `<title>${escapeHtml(metadata.title || "Test")}</title>`,
24
+ '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">',
25
+ '<meta name="robots" content="noindex">',
26
+ '<meta name="apple-mobile-web-app-capable" content="yes">',
27
+ faviconTag
28
+ ].join("\n");
29
+ }
30
+ const tags = Object.entries(metadata).reduce(
31
+ (acc, [key, value]) => {
32
+ if (!value && value !== 0 && value !== false) return acc;
33
+ if (key === "title") {
34
+ acc.push(`<title>${escapeHtml(value)}</title>`);
35
+ return acc;
36
+ }
37
+ if (key === "canonical") {
38
+ acc.push(`<link rel="canonical" href="${escapeHtml(value)}">`);
39
+ return acc;
40
+ }
41
+ if (key === "manifest") {
42
+ acc.push(`<link rel="manifest" href="${escapeHtml(value)}">`);
43
+ return acc;
44
+ }
45
+ if (key === "favicon" || key === "favicons" || key === "icon" || key === "icons") {
46
+ const items = Array.isArray(value) ? value : [value];
47
+ items.forEach((item) => {
48
+ if (typeof item === "string") {
49
+ acc.push(`<link rel="icon" href="${escapeHtml(item)}">`);
50
+ } else if (typeof item === "object") {
51
+ const attrs = buildAttrs(item);
52
+ if (!/rel=/.test(attrs)) {
53
+ acc.push(`<link rel="icon" ${attrs}>`);
54
+ } else {
55
+ acc.push(`<link ${attrs}>`);
56
+ }
57
+ }
58
+ });
59
+ return acc;
60
+ }
61
+ if (key === "alternate") {
62
+ const alternates = Array.isArray(value) ? value : [value];
63
+ alternates.forEach((alt) => {
64
+ if (typeof alt === "object") {
65
+ const attrs = Object.entries(alt).filter(([_, attrValue]) => attrValue !== void 0).map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`).join(" ");
66
+ acc.push(`<link rel="alternate" ${attrs}>`);
67
+ }
68
+ });
69
+ return acc;
70
+ }
71
+ const processMetaTag = (tagKey, tagValue, attrType = "name") => {
72
+ if (typeof tagValue === "string" || typeof tagValue === "number" || typeof tagValue === "boolean") {
73
+ acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(tagValue)}">`);
74
+ } else if (Array.isArray(tagValue)) {
75
+ tagValue.forEach((item) => {
76
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
77
+ acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(item)}">`);
78
+ } else if (typeof item === "object") {
79
+ const attrs = Object.entries(item).filter(([_, attrValue]) => attrValue !== void 0).map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`).join(" ");
80
+ acc.push(`<meta ${attrs}>`);
81
+ }
82
+ });
83
+ } else if (typeof tagValue === "object") {
84
+ const attrs = Object.entries(tagValue).filter(([_, attrValue]) => attrValue !== void 0).map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`).join(" ");
85
+ acc.push(`<meta ${attrs}>`);
86
+ }
87
+ };
88
+ if (key.startsWith("http-equiv:")) {
89
+ const httpEquivKey = key.replace("http-equiv:", "");
90
+ processMetaTag(httpEquivKey, value, "http-equiv");
91
+ return acc;
92
+ }
93
+ if (key.startsWith("itemprop:")) {
94
+ const itempropKey = key.replace("itemprop:", "");
95
+ processMetaTag(itempropKey, value, "itemprop");
96
+ return acc;
97
+ }
98
+ const prefixes = [
99
+ "og:",
100
+ "twitter:",
101
+ "fb:",
102
+ "article:",
103
+ "profile:",
104
+ "book:",
105
+ "business:",
106
+ "music:",
107
+ "product:",
108
+ "video:",
109
+ "DC:",
110
+ "DCTERMS:"
111
+ ];
112
+ const prefix = prefixes.find((p) => key.startsWith(p));
113
+ if (prefix) {
114
+ const tagKey = key.replace(prefix, "");
115
+ processMetaTag(
116
+ `${prefix.replace(":", "")}:${tagKey}`,
117
+ value,
118
+ prefix === "twitter:" || prefix === "DC:" || prefix === "DCTERMS:" ? "name" : "property"
119
+ );
120
+ return acc;
121
+ }
122
+ if (key.startsWith("apple:") || key.startsWith("msapplication:")) {
123
+ const pfx = key.startsWith("apple:") ? "apple-" : "msapplication-";
124
+ const tagKey = key.replace(/^apple:|^msapplication:/, "");
125
+ processMetaTag(`${pfx}${tagKey}`, value, "name");
126
+ return acc;
127
+ }
128
+ processMetaTag(key, value);
129
+ return acc;
130
+ },
131
+ [
132
+ '<meta charset="UTF-8">',
133
+ '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">'
134
+ ]
135
+ );
136
+ return tags.join("\n");
137
+ };
138
+ const isBareFilename = (val) => typeof val === "string" && val.length > 0 && !val.startsWith("/") && !val.startsWith("http");
139
+ function resolveFileReferences(metadata, files) {
140
+ if (!files || typeof files !== "object") return metadata;
141
+ const resolve = (val) => {
142
+ if (!isBareFilename(val)) return val;
143
+ const fileEntry = files[val];
144
+ if (fileEntry?.src) return fileEntry.src;
145
+ return val;
146
+ };
147
+ const result = { ...metadata };
148
+ for (const [key, value] of Object.entries(result)) {
149
+ if (typeof value === "string") {
150
+ result[key] = resolve(value);
151
+ } else if (Array.isArray(value)) {
152
+ result[key] = value.map(
153
+ (item) => typeof item === "string" ? resolve(item) : item
154
+ );
155
+ }
156
+ }
157
+ return result;
158
+ }
159
+ function getPageMetadata(data, pathname) {
160
+ const currentPage = data.pages?.[pathname];
161
+ const stateObject = isObject(currentPage?.state) && currentPage?.state;
162
+ let pageMetadata = currentPage?.metadata || currentPage?.helmet || stateObject || {};
163
+ const appMetadata = data.app?.metadata || {};
164
+ if (data.integrations?.seo) {
165
+ pageMetadata = { ...data.integrations.seo, ...appMetadata, ...pageMetadata };
166
+ } else if (Object.keys(appMetadata).length) {
167
+ pageMetadata = { ...appMetadata, ...pageMetadata };
168
+ }
169
+ if (!pageMetadata.title) pageMetadata.title = data.name + " / symbo.ls" || "Symbols demo";
170
+ pageMetadata = resolveFileReferences(pageMetadata, data.files);
171
+ return pageMetadata;
172
+ }
173
+ export {
174
+ generateMetaTags,
175
+ getPageMetadata
176
+ };
@@ -21,6 +21,8 @@ var SmblsSmblsUtils = (() => {
21
21
  // src/index.js
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ CDN_PROVIDERS: () => CDN_PROVIDERS,
25
+ PACKAGE_MANAGER_TO_CDN: () => PACKAGE_MANAGER_TO_CDN,
24
26
  arrayzeValue: () => arrayzeValue,
25
27
  copyJavaScriptToClipboard: () => copyJavaScriptToClipboard,
26
28
  copyStringToClipboard: () => copyStringToClipboard,
@@ -28,6 +30,11 @@ var SmblsSmblsUtils = (() => {
28
30
  findClosestNumber: () => findClosestNumber,
29
31
  findClosestNumberInFactory: () => findClosestNumberInFactory,
30
32
  formatDate: () => formatDate,
33
+ generateMetaTags: () => generateMetaTags,
34
+ getCDNUrl: () => getCDNUrl,
35
+ getCdnProviderFromConfig: () => getCdnProviderFromConfig,
36
+ getImportMapScript: () => getImportMapScript,
37
+ getPageMetadata: () => getPageMetadata,
31
38
  isPhoto: () => isPhoto,
32
39
  loadCssFile: () => loadCssFile,
33
40
  loadJavascript: () => loadJavascript,
@@ -244,6 +251,239 @@ var SmblsSmblsUtils = (() => {
244
251
  // src/files.js
245
252
  var isPhoto = (format) => ["jpeg", "gif", "jpg", "png", "tiff", "woff"].includes(format);
246
253
 
254
+ // src/cdn.js
255
+ function onlyDotsAndNumbers(str) {
256
+ return /^[0-9.]+$/.test(str) && str !== "";
257
+ }
258
+ var CDN_PROVIDERS = {
259
+ skypack: {
260
+ url: "https://cdn.skypack.dev",
261
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.skypack.url}/${pkg}${version !== "latest" ? `@${version}` : ""}`
262
+ },
263
+ esmsh: {
264
+ url: "https://esm.sh",
265
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.esmsh.url}/${pkg}${version !== "latest" ? `@${version}` : ""}`
266
+ },
267
+ unpkg: {
268
+ url: "https://unpkg.com",
269
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.unpkg.url}/${pkg}${version !== "latest" ? `@${version}` : ""}?module`
270
+ },
271
+ jsdelivr: {
272
+ url: "https://cdn.jsdelivr.net/npm",
273
+ formatUrl: (pkg, version) => `${CDN_PROVIDERS.jsdelivr.url}/${pkg}${version !== "latest" ? `@${version}` : ""}/+esm`
274
+ },
275
+ symbols: {
276
+ url: "https://pkg.symbo.ls",
277
+ formatUrl: (pkg, version) => {
278
+ if (pkg.split("/").length > 2 || !onlyDotsAndNumbers(version)) {
279
+ return `${CDN_PROVIDERS.symbols.url}/${pkg}`;
280
+ }
281
+ return `${CDN_PROVIDERS.symbols.url}/${pkg}/${version}.js`;
282
+ }
283
+ }
284
+ };
285
+ var PACKAGE_MANAGER_TO_CDN = {
286
+ "esm.sh": "esmsh",
287
+ "unpkg": "unpkg",
288
+ "skypack": "skypack",
289
+ "jsdelivr": "jsdelivr",
290
+ "pkg.symbo.ls": "symbols"
291
+ };
292
+ var getCdnProviderFromConfig = (symbolsConfig = {}) => {
293
+ const { packageManager } = symbolsConfig;
294
+ return PACKAGE_MANAGER_TO_CDN[packageManager] || null;
295
+ };
296
+ var getCDNUrl = (packageName, version = "latest", provider = "esmsh") => {
297
+ const cdnConfig = CDN_PROVIDERS[provider] || CDN_PROVIDERS.esmsh;
298
+ return cdnConfig.formatUrl(packageName, version);
299
+ };
300
+ var getImportMapScript = (data, defaultProvider = "skypack") => {
301
+ const dependencies = data.dependencies || {};
302
+ const keys = Object.keys(dependencies);
303
+ if (!keys.length) return "";
304
+ const imports = {};
305
+ for (const pkgName of keys) {
306
+ const version = dependencies[pkgName] || "latest";
307
+ imports[pkgName] = getCDNUrl(pkgName, version, defaultProvider);
308
+ }
309
+ return `<script type="importmap">{
310
+ "imports": ${JSON.stringify(imports, null, 2)}
311
+ }<\/script>`;
312
+ };
313
+
314
+ // src/metadata.js
315
+ var escapeHtml = (text) => {
316
+ if (text === null || text === void 0) return "";
317
+ const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
318
+ return text.toString().replace(/[&<>"']/g, (m) => map[m]);
319
+ };
320
+ var buildAttrs = (obj) => Object.entries(obj || {}).filter(([_, v]) => v !== void 0).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(" ");
321
+ var generateMetaTags = (metadata, isProduction) => {
322
+ if (!isProduction) {
323
+ const faviconTag = (() => {
324
+ const fv = metadata?.favicon || metadata?.favicons;
325
+ if (!fv) return '<link rel="icon" href="/favicon.ico">';
326
+ if (typeof fv === "string") return `<link rel="icon" href="${escapeHtml(fv)}">`;
327
+ if (Array.isArray(fv)) {
328
+ return fv.map(
329
+ (item) => typeof item === "string" ? `<link rel="icon" href="${escapeHtml(item)}">` : `<link ${buildAttrs({ rel: "icon", ...item })}>`
330
+ ).join("\n");
331
+ }
332
+ return `<link ${buildAttrs({ rel: "icon", ...fv })}>`;
333
+ })();
334
+ return [
335
+ '<meta charset="UTF-8">',
336
+ `<title>${escapeHtml(metadata.title || "Test")}</title>`,
337
+ '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">',
338
+ '<meta name="robots" content="noindex">',
339
+ '<meta name="apple-mobile-web-app-capable" content="yes">',
340
+ faviconTag
341
+ ].join("\n");
342
+ }
343
+ const tags = Object.entries(metadata).reduce(
344
+ (acc, [key, value]) => {
345
+ if (!value && value !== 0 && value !== false) return acc;
346
+ if (key === "title") {
347
+ acc.push(`<title>${escapeHtml(value)}</title>`);
348
+ return acc;
349
+ }
350
+ if (key === "canonical") {
351
+ acc.push(`<link rel="canonical" href="${escapeHtml(value)}">`);
352
+ return acc;
353
+ }
354
+ if (key === "manifest") {
355
+ acc.push(`<link rel="manifest" href="${escapeHtml(value)}">`);
356
+ return acc;
357
+ }
358
+ if (key === "favicon" || key === "favicons" || key === "icon" || key === "icons") {
359
+ const items = Array.isArray(value) ? value : [value];
360
+ items.forEach((item) => {
361
+ if (typeof item === "string") {
362
+ acc.push(`<link rel="icon" href="${escapeHtml(item)}">`);
363
+ } else if (typeof item === "object") {
364
+ const attrs = buildAttrs(item);
365
+ if (!/rel=/.test(attrs)) {
366
+ acc.push(`<link rel="icon" ${attrs}>`);
367
+ } else {
368
+ acc.push(`<link ${attrs}>`);
369
+ }
370
+ }
371
+ });
372
+ return acc;
373
+ }
374
+ if (key === "alternate") {
375
+ const alternates = Array.isArray(value) ? value : [value];
376
+ alternates.forEach((alt) => {
377
+ if (typeof alt === "object") {
378
+ const attrs = Object.entries(alt).filter(([_, attrValue]) => attrValue !== void 0).map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`).join(" ");
379
+ acc.push(`<link rel="alternate" ${attrs}>`);
380
+ }
381
+ });
382
+ return acc;
383
+ }
384
+ const processMetaTag = (tagKey, tagValue, attrType = "name") => {
385
+ if (typeof tagValue === "string" || typeof tagValue === "number" || typeof tagValue === "boolean") {
386
+ acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(tagValue)}">`);
387
+ } else if (Array.isArray(tagValue)) {
388
+ tagValue.forEach((item) => {
389
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
390
+ acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(item)}">`);
391
+ } else if (typeof item === "object") {
392
+ const attrs = Object.entries(item).filter(([_, attrValue]) => attrValue !== void 0).map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`).join(" ");
393
+ acc.push(`<meta ${attrs}>`);
394
+ }
395
+ });
396
+ } else if (typeof tagValue === "object") {
397
+ const attrs = Object.entries(tagValue).filter(([_, attrValue]) => attrValue !== void 0).map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`).join(" ");
398
+ acc.push(`<meta ${attrs}>`);
399
+ }
400
+ };
401
+ if (key.startsWith("http-equiv:")) {
402
+ const httpEquivKey = key.replace("http-equiv:", "");
403
+ processMetaTag(httpEquivKey, value, "http-equiv");
404
+ return acc;
405
+ }
406
+ if (key.startsWith("itemprop:")) {
407
+ const itempropKey = key.replace("itemprop:", "");
408
+ processMetaTag(itempropKey, value, "itemprop");
409
+ return acc;
410
+ }
411
+ const prefixes = [
412
+ "og:",
413
+ "twitter:",
414
+ "fb:",
415
+ "article:",
416
+ "profile:",
417
+ "book:",
418
+ "business:",
419
+ "music:",
420
+ "product:",
421
+ "video:",
422
+ "DC:",
423
+ "DCTERMS:"
424
+ ];
425
+ const prefix = prefixes.find((p) => key.startsWith(p));
426
+ if (prefix) {
427
+ const tagKey = key.replace(prefix, "");
428
+ processMetaTag(
429
+ `${prefix.replace(":", "")}:${tagKey}`,
430
+ value,
431
+ prefix === "twitter:" || prefix === "DC:" || prefix === "DCTERMS:" ? "name" : "property"
432
+ );
433
+ return acc;
434
+ }
435
+ if (key.startsWith("apple:") || key.startsWith("msapplication:")) {
436
+ const pfx = key.startsWith("apple:") ? "apple-" : "msapplication-";
437
+ const tagKey = key.replace(/^apple:|^msapplication:/, "");
438
+ processMetaTag(`${pfx}${tagKey}`, value, "name");
439
+ return acc;
440
+ }
441
+ processMetaTag(key, value);
442
+ return acc;
443
+ },
444
+ [
445
+ '<meta charset="UTF-8">',
446
+ '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">'
447
+ ]
448
+ );
449
+ return tags.join("\n");
450
+ };
451
+ var isBareFilename = (val) => typeof val === "string" && val.length > 0 && !val.startsWith("/") && !val.startsWith("http");
452
+ function resolveFileReferences(metadata, files) {
453
+ if (!files || typeof files !== "object") return metadata;
454
+ const resolve = (val) => {
455
+ if (!isBareFilename(val)) return val;
456
+ const fileEntry = files[val];
457
+ if (fileEntry?.src) return fileEntry.src;
458
+ return val;
459
+ };
460
+ const result = { ...metadata };
461
+ for (const [key, value] of Object.entries(result)) {
462
+ if (typeof value === "string") {
463
+ result[key] = resolve(value);
464
+ } else if (Array.isArray(value)) {
465
+ result[key] = value.map(
466
+ (item) => typeof item === "string" ? resolve(item) : item
467
+ );
468
+ }
469
+ }
470
+ return result;
471
+ }
472
+ function getPageMetadata(data, pathname) {
473
+ const currentPage = data.pages?.[pathname];
474
+ const stateObject = isObject(currentPage?.state) && currentPage?.state;
475
+ let pageMetadata = currentPage?.metadata || currentPage?.helmet || stateObject || {};
476
+ const appMetadata = data.app?.metadata || {};
477
+ if (data.integrations?.seo) {
478
+ pageMetadata = { ...data.integrations.seo, ...appMetadata, ...pageMetadata };
479
+ } else if (Object.keys(appMetadata).length) {
480
+ pageMetadata = { ...appMetadata, ...pageMetadata };
481
+ }
482
+ if (!pageMetadata.title) pageMetadata.title = data.name + " / symbo.ls" || "Symbols demo";
483
+ pageMetadata = resolveFileReferences(pageMetadata, data.files);
484
+ return pageMetadata;
485
+ }
486
+
247
487
  // src/index.js
248
488
  var copyStringToClipboard = async (str) => {
249
489
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/smbls-utils",
3
- "version": "3.5.1",
3
+ "version": "3.6.3",
4
4
  "author": "symbo.ls",
5
5
  "files": [
6
6
  "dist",
@@ -15,13 +15,13 @@
15
15
  "exports": {
16
16
  ".": {
17
17
  "import": "./dist/esm/index.js",
18
- "require": "./dist/cjs/index.js",
19
- "browser": "./dist/esm/index.js",
20
- "default": "./dist/esm/index.js"
18
+ "require": "./dist/cjs/index.js"
21
19
  }
22
20
  },
23
21
  "source": "src/index.js",
24
- "publishConfig": {},
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
25
  "scripts": {
26
26
  "copy:package:cjs": "cp ../../build/package-cjs.json dist/cjs/package.json",
27
27
  "build:esm": "cross-env NODE_ENV=$NODE_ENV esbuild src/*.js --target=es2020 --format=esm --outdir=dist/esm --define:process.env.NODE_ENV=process.env.NODE_ENV",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "license": "ISC",
34
34
  "dependencies": {
35
- "@domql/element": "^3.5.1",
36
- "@domql/utils": "^3.5.1"
35
+ "@domql/element": "^3.6.3",
36
+ "@domql/utils": "^3.6.3"
37
37
  },
38
38
  "gitHead": "9fc1b79b41cdc725ca6b24aec64920a599634681",
39
39
  "browser": "./dist/esm/index.js",
package/src/cdn.js ADDED
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ function onlyDotsAndNumbers (str) {
4
+ return /^[0-9.]+$/.test(str) && str !== ''
5
+ }
6
+
7
+ export const CDN_PROVIDERS = {
8
+ skypack: {
9
+ url: 'https://cdn.skypack.dev',
10
+ formatUrl: (pkg, version) =>
11
+ `${CDN_PROVIDERS.skypack.url}/${pkg}${version !== 'latest' ? `@${version}` : ''}`
12
+ },
13
+ esmsh: {
14
+ url: 'https://esm.sh',
15
+ formatUrl: (pkg, version) =>
16
+ `${CDN_PROVIDERS.esmsh.url}/${pkg}${version !== 'latest' ? `@${version}` : ''}`
17
+ },
18
+ unpkg: {
19
+ url: 'https://unpkg.com',
20
+ formatUrl: (pkg, version) =>
21
+ `${CDN_PROVIDERS.unpkg.url}/${pkg}${version !== 'latest' ? `@${version}` : ''}?module`
22
+ },
23
+ jsdelivr: {
24
+ url: 'https://cdn.jsdelivr.net/npm',
25
+ formatUrl: (pkg, version) =>
26
+ `${CDN_PROVIDERS.jsdelivr.url}/${pkg}${version !== 'latest' ? `@${version}` : ''}/+esm`
27
+ },
28
+ symbols: {
29
+ url: 'https://pkg.symbo.ls',
30
+ formatUrl: (pkg, version) => {
31
+ if (pkg.split('/').length > 2 || !onlyDotsAndNumbers(version)) {
32
+ return `${CDN_PROVIDERS.symbols.url}/${pkg}`
33
+ }
34
+ return `${CDN_PROVIDERS.symbols.url}/${pkg}/${version}.js`
35
+ }
36
+ }
37
+ }
38
+
39
+ // Maps symbols.json packageManager values to CDN_PROVIDERS keys
40
+ export const PACKAGE_MANAGER_TO_CDN = {
41
+ 'esm.sh': 'esmsh',
42
+ 'unpkg': 'unpkg',
43
+ 'skypack': 'skypack',
44
+ 'jsdelivr': 'jsdelivr',
45
+ 'pkg.symbo.ls': 'symbols'
46
+ }
47
+
48
+ /**
49
+ * Derive the CDN provider key from a symbols config object.
50
+ * Returns null when packageManager is a local tool (npm/yarn/pnpm/bun).
51
+ */
52
+ export const getCdnProviderFromConfig = (symbolsConfig = {}) => {
53
+ const { packageManager } = symbolsConfig
54
+ return PACKAGE_MANAGER_TO_CDN[packageManager] || null
55
+ }
56
+
57
+ export const getCDNUrl = (
58
+ packageName,
59
+ version = 'latest',
60
+ provider = 'esmsh'
61
+ ) => {
62
+ const cdnConfig = CDN_PROVIDERS[provider] || CDN_PROVIDERS.esmsh
63
+ return cdnConfig.formatUrl(packageName, version)
64
+ }
65
+
66
+ /**
67
+ * Generate an HTML <script type="importmap"> tag from project dependencies.
68
+ */
69
+ export const getImportMapScript = (data, defaultProvider = 'skypack') => {
70
+ const dependencies = data.dependencies || {}
71
+ const keys = Object.keys(dependencies)
72
+ if (!keys.length) return ''
73
+
74
+ const imports = {}
75
+ for (const pkgName of keys) {
76
+ const version = dependencies[pkgName] || 'latest'
77
+ imports[pkgName] = getCDNUrl(pkgName, version, defaultProvider)
78
+ }
79
+
80
+ return `<script type="importmap">{
81
+ "imports": ${JSON.stringify(imports, null, 2)}
82
+ }</script>`
83
+ }
package/src/index.js CHANGED
@@ -8,6 +8,8 @@ export * from './date.js'
8
8
  export * from './fibonacci.js'
9
9
  export * from './load.js'
10
10
  export * from './files.js'
11
+ export * from './cdn.js'
12
+ export * from './metadata.js'
11
13
 
12
14
  export const copyStringToClipboard = async (str) => {
13
15
  try {
@@ -0,0 +1,225 @@
1
+ 'use strict'
2
+
3
+ import { isObject } from '@domql/utils'
4
+
5
+ const escapeHtml = (text) => {
6
+ if (text === null || text === undefined) return ''
7
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }
8
+ return text.toString().replace(/[&<>"']/g, (m) => map[m])
9
+ }
10
+
11
+ const buildAttrs = (obj) =>
12
+ Object.entries(obj || {})
13
+ .filter(([_, v]) => v !== undefined)
14
+ .map(([k, v]) => `${k}="${escapeHtml(v)}"`)
15
+ .join(' ')
16
+
17
+ /**
18
+ * Generates HTML meta tags from a flat metadata object.
19
+ * Works on both server (SSR head injection) and client (dynamic meta updates).
20
+ */
21
+ export const generateMetaTags = (metadata, isProduction) => {
22
+ if (!isProduction) {
23
+ const faviconTag = (() => {
24
+ const fv = metadata?.favicon || metadata?.favicons
25
+ if (!fv) return '<link rel="icon" href="/favicon.ico">'
26
+ if (typeof fv === 'string') return `<link rel="icon" href="${escapeHtml(fv)}">`
27
+ if (Array.isArray(fv)) {
28
+ return fv
29
+ .map((item) =>
30
+ typeof item === 'string'
31
+ ? `<link rel="icon" href="${escapeHtml(item)}">`
32
+ : `<link ${buildAttrs({ rel: 'icon', ...item })}>`
33
+ )
34
+ .join('\n')
35
+ }
36
+ return `<link ${buildAttrs({ rel: 'icon', ...fv })}>`
37
+ })()
38
+
39
+ return [
40
+ '<meta charset="UTF-8">',
41
+ `<title>${escapeHtml(metadata.title || 'Test')}</title>`,
42
+ '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">',
43
+ '<meta name="robots" content="noindex">',
44
+ '<meta name="apple-mobile-web-app-capable" content="yes">',
45
+ faviconTag
46
+ ].join('\n')
47
+ }
48
+
49
+ const tags = Object.entries(metadata).reduce(
50
+ (acc, [key, value]) => {
51
+ if (!value && value !== 0 && value !== false) return acc
52
+
53
+ if (key === 'title') {
54
+ acc.push(`<title>${escapeHtml(value)}</title>`)
55
+ return acc
56
+ }
57
+
58
+ if (key === 'canonical') {
59
+ acc.push(`<link rel="canonical" href="${escapeHtml(value)}">`)
60
+ return acc
61
+ }
62
+
63
+ if (key === 'manifest') {
64
+ acc.push(`<link rel="manifest" href="${escapeHtml(value)}">`)
65
+ return acc
66
+ }
67
+
68
+ if (key === 'favicon' || key === 'favicons' || key === 'icon' || key === 'icons') {
69
+ const items = Array.isArray(value) ? value : [value]
70
+ items.forEach((item) => {
71
+ if (typeof item === 'string') {
72
+ acc.push(`<link rel="icon" href="${escapeHtml(item)}">`)
73
+ } else if (typeof item === 'object') {
74
+ const attrs = buildAttrs(item)
75
+ if (!/rel=/.test(attrs)) {
76
+ acc.push(`<link rel="icon" ${attrs}>`)
77
+ } else {
78
+ acc.push(`<link ${attrs}>`)
79
+ }
80
+ }
81
+ })
82
+ return acc
83
+ }
84
+
85
+ if (key === 'alternate') {
86
+ const alternates = Array.isArray(value) ? value : [value]
87
+ alternates.forEach((alt) => {
88
+ if (typeof alt === 'object') {
89
+ const attrs = Object.entries(alt)
90
+ .filter(([_, attrValue]) => attrValue !== undefined)
91
+ .map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`)
92
+ .join(' ')
93
+ acc.push(`<link rel="alternate" ${attrs}>`)
94
+ }
95
+ })
96
+ return acc
97
+ }
98
+
99
+ const processMetaTag = (tagKey, tagValue, attrType = 'name') => {
100
+ if (
101
+ typeof tagValue === 'string' ||
102
+ typeof tagValue === 'number' ||
103
+ typeof tagValue === 'boolean'
104
+ ) {
105
+ acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(tagValue)}">`)
106
+ } else if (Array.isArray(tagValue)) {
107
+ tagValue.forEach((item) => {
108
+ if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
109
+ acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(item)}">`)
110
+ } else if (typeof item === 'object') {
111
+ const attrs = Object.entries(item)
112
+ .filter(([_, attrValue]) => attrValue !== undefined)
113
+ .map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`)
114
+ .join(' ')
115
+ acc.push(`<meta ${attrs}>`)
116
+ }
117
+ })
118
+ } else if (typeof tagValue === 'object') {
119
+ const attrs = Object.entries(tagValue)
120
+ .filter(([_, attrValue]) => attrValue !== undefined)
121
+ .map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`)
122
+ .join(' ')
123
+ acc.push(`<meta ${attrs}>`)
124
+ }
125
+ }
126
+
127
+ if (key.startsWith('http-equiv:')) {
128
+ const httpEquivKey = key.replace('http-equiv:', '')
129
+ processMetaTag(httpEquivKey, value, 'http-equiv')
130
+ return acc
131
+ }
132
+
133
+ if (key.startsWith('itemprop:')) {
134
+ const itempropKey = key.replace('itemprop:', '')
135
+ processMetaTag(itempropKey, value, 'itemprop')
136
+ return acc
137
+ }
138
+
139
+ const prefixes = [
140
+ 'og:', 'twitter:', 'fb:', 'article:', 'profile:', 'book:',
141
+ 'business:', 'music:', 'product:', 'video:', 'DC:', 'DCTERMS:'
142
+ ]
143
+ const prefix = prefixes.find((p) => key.startsWith(p))
144
+ if (prefix) {
145
+ const tagKey = key.replace(prefix, '')
146
+ processMetaTag(
147
+ `${prefix.replace(':', '')}:${tagKey}`,
148
+ value,
149
+ prefix === 'twitter:' || prefix === 'DC:' || prefix === 'DCTERMS:' ? 'name' : 'property'
150
+ )
151
+ return acc
152
+ }
153
+
154
+ if (key.startsWith('apple:') || key.startsWith('msapplication:')) {
155
+ const pfx = key.startsWith('apple:') ? 'apple-' : 'msapplication-'
156
+ const tagKey = key.replace(/^apple:|^msapplication:/, '')
157
+ processMetaTag(`${pfx}${tagKey}`, value, 'name')
158
+ return acc
159
+ }
160
+
161
+ processMetaTag(key, value)
162
+ return acc
163
+ },
164
+ [
165
+ '<meta charset="UTF-8">',
166
+ '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">'
167
+ ]
168
+ )
169
+
170
+ return tags.join('\n')
171
+ }
172
+
173
+ /**
174
+ * Extract page-level metadata from project data for a given pathname.
175
+ * Merges global SEO (data.integrations.seo) with page-level metadata/helmet/state.
176
+ */
177
+ /**
178
+ * Check if a string looks like a bare filename (not an absolute path or URL).
179
+ * These are references to project files from `data.files`.
180
+ */
181
+ const isBareFilename = (val) =>
182
+ typeof val === 'string' && val.length > 0 && !val.startsWith('/') && !val.startsWith('http')
183
+
184
+ /**
185
+ * Resolve bare filename references in metadata values against `data.files`.
186
+ * If a metadata value is a plain filename (e.g. "logo.png") and a matching
187
+ * entry exists in `data.files`, replace it with the file's `src` URL.
188
+ */
189
+ function resolveFileReferences (metadata, files) {
190
+ if (!files || typeof files !== 'object') return metadata
191
+
192
+ const resolve = (val) => {
193
+ if (!isBareFilename(val)) return val
194
+ const fileEntry = files[val]
195
+ if (fileEntry?.src) return fileEntry.src
196
+ return val
197
+ }
198
+
199
+ const result = { ...metadata }
200
+ for (const [key, value] of Object.entries(result)) {
201
+ if (typeof value === 'string') {
202
+ result[key] = resolve(value)
203
+ } else if (Array.isArray(value)) {
204
+ result[key] = value.map((item) =>
205
+ typeof item === 'string' ? resolve(item) : item
206
+ )
207
+ }
208
+ }
209
+ return result
210
+ }
211
+
212
+ export function getPageMetadata (data, pathname) {
213
+ const currentPage = data.pages?.[pathname]
214
+ const stateObject = isObject(currentPage?.state) && currentPage?.state
215
+ let pageMetadata = currentPage?.metadata || currentPage?.helmet || stateObject || {}
216
+ const appMetadata = data.app?.metadata || {}
217
+ if (data.integrations?.seo) {
218
+ pageMetadata = { ...data.integrations.seo, ...appMetadata, ...pageMetadata }
219
+ } else if (Object.keys(appMetadata).length) {
220
+ pageMetadata = { ...appMetadata, ...pageMetadata }
221
+ }
222
+ if (!pageMetadata.title) pageMetadata.title = data.name + ' / symbo.ls' || 'Symbols demo'
223
+ pageMetadata = resolveFileReferences(pageMetadata, data.files)
224
+ return pageMetadata
225
+ }