@symbo.ls/utils 3.2.3 → 3.14.1
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/LICENSE +25 -12
- package/README.md +106 -0
- package/array.js +162 -0
- package/assets.js +117 -0
- package/cache.js +7 -0
- package/cdn.js +83 -0
- package/component.js +25 -0
- package/cookie.js +78 -0
- package/{src/detectHeight.js → detectHeight.js} +3 -3
- package/dist/cjs/detectHeight.js +10 -21
- package/dist/cjs/domqlv3hacks.js +3845 -0
- package/dist/cjs/index.js +3910 -251
- package/dist/cjs/scaling.js +3 -149
- package/element.js +148 -0
- package/env.js +15 -0
- package/events.js +19 -0
- package/extends.js +519 -0
- package/function.js +145 -0
- package/globals.js +8 -0
- package/if.js +30 -0
- package/index.js +42 -0
- package/key.js +20 -0
- package/keys.js +176 -0
- package/log.js +12 -0
- package/metadata.js +392 -0
- package/methods.js +684 -0
- package/node.js +37 -0
- package/object.js +837 -0
- package/package.json +38 -22
- package/projectKeys.js +30 -0
- package/props.js +327 -0
- package/{src/scaling.js → scaling.js} +1 -1
- package/scope.js +32 -0
- package/sharedLibraries.js +184 -0
- package/state.js +203 -0
- package/{src/index.js → string-extra.js} +11 -17
- package/string.js +206 -0
- package/tags.js +176 -0
- package/triggerEvent.js +102 -0
- package/types.js +55 -0
- package/update.js +27 -0
- /package/{src/browser.js → browser.js} +0 -0
- /package/{src/date.js → date.js} +0 -0
- /package/{src/fibonacci.js → fibonacci.js} +0 -0
- /package/{src/files.js → files.js} +0 -0
- /package/{src/load.js → load.js} +0 -0
- /package/{src/style.js → style.js} +0 -0
package/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
export * from './key.js'
|
|
4
|
+
export * from './env.js'
|
|
5
|
+
export * from './types.js'
|
|
6
|
+
export * from './object.js'
|
|
7
|
+
export * from './function.js'
|
|
8
|
+
export * from './array.js'
|
|
9
|
+
export * from './node.js'
|
|
10
|
+
export * from './if.js'
|
|
11
|
+
export * from './log.js'
|
|
12
|
+
export * from './string.js'
|
|
13
|
+
export * from './globals.js'
|
|
14
|
+
export * from './cookie.js'
|
|
15
|
+
export * from './tags.js'
|
|
16
|
+
export * from './component.js'
|
|
17
|
+
export * from './props.js'
|
|
18
|
+
export * from './extends.js'
|
|
19
|
+
export * from './element.js'
|
|
20
|
+
export * from './state.js'
|
|
21
|
+
export * from './keys.js'
|
|
22
|
+
export * from './scope.js'
|
|
23
|
+
export * from './methods.js'
|
|
24
|
+
export * from './cache.js'
|
|
25
|
+
export * from './update.js'
|
|
26
|
+
export * from './triggerEvent.js'
|
|
27
|
+
export * from './projectKeys.js'
|
|
28
|
+
|
|
29
|
+
// ── Merged from the former @symbo.ls/smbls-utils package (4.x) ────────────
|
|
30
|
+
export * from './browser.js'
|
|
31
|
+
export * from './cdn.js'
|
|
32
|
+
export * from './date.js'
|
|
33
|
+
export * from './detectHeight.js'
|
|
34
|
+
export * from './fibonacci.js'
|
|
35
|
+
export * from './files.js'
|
|
36
|
+
export * from './load.js'
|
|
37
|
+
export * from './metadata.js'
|
|
38
|
+
export * from './scaling.js'
|
|
39
|
+
export * from './sharedLibraries.js'
|
|
40
|
+
export * from './string-extra.js'
|
|
41
|
+
export * from './style.js'
|
|
42
|
+
export * from './assets.js'
|
package/key.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import { exec } from './object.js'
|
|
4
|
+
|
|
5
|
+
export const generateKey = (function () {
|
|
6
|
+
let index = 0
|
|
7
|
+
|
|
8
|
+
function newId () {
|
|
9
|
+
index++
|
|
10
|
+
return index
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return newId
|
|
14
|
+
})()
|
|
15
|
+
|
|
16
|
+
export const createSnapshotId = generateKey
|
|
17
|
+
|
|
18
|
+
export const createKey = (element, parent, key) => {
|
|
19
|
+
return (exec(key, element) || key || element.key || generateKey()).toString()
|
|
20
|
+
}
|
package/keys.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
export const DOMQ_PROPERTIES = new Set([
|
|
4
|
+
'attr',
|
|
5
|
+
'style',
|
|
6
|
+
'text',
|
|
7
|
+
'html',
|
|
8
|
+
'content',
|
|
9
|
+
'data',
|
|
10
|
+
'classlist',
|
|
11
|
+
'state',
|
|
12
|
+
'scope',
|
|
13
|
+
'root',
|
|
14
|
+
'deps',
|
|
15
|
+
'extend',
|
|
16
|
+
'extends',
|
|
17
|
+
'$router',
|
|
18
|
+
'routes',
|
|
19
|
+
'children',
|
|
20
|
+
'childExtend',
|
|
21
|
+
'childExtends',
|
|
22
|
+
'childExtendRecursive',
|
|
23
|
+
'childExtendsRecursive',
|
|
24
|
+
'props',
|
|
25
|
+
'if',
|
|
26
|
+
'define',
|
|
27
|
+
'__name',
|
|
28
|
+
'__ref',
|
|
29
|
+
'__hash',
|
|
30
|
+
'__text',
|
|
31
|
+
'key',
|
|
32
|
+
'tag',
|
|
33
|
+
'query',
|
|
34
|
+
'parent',
|
|
35
|
+
'node',
|
|
36
|
+
'variables',
|
|
37
|
+
'on',
|
|
38
|
+
'fetch',
|
|
39
|
+
'component',
|
|
40
|
+
'context',
|
|
41
|
+
'preventContentUpdate',
|
|
42
|
+
'preventContentRecreate'
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
export const PARSED_DOMQ_PROPERTIES = new Set([
|
|
46
|
+
'attr',
|
|
47
|
+
'style',
|
|
48
|
+
'text',
|
|
49
|
+
'html',
|
|
50
|
+
'content',
|
|
51
|
+
'data',
|
|
52
|
+
'class',
|
|
53
|
+
'state',
|
|
54
|
+
'scope',
|
|
55
|
+
'children',
|
|
56
|
+
'props',
|
|
57
|
+
'if',
|
|
58
|
+
'key',
|
|
59
|
+
'tag',
|
|
60
|
+
'query',
|
|
61
|
+
'on',
|
|
62
|
+
'context'
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
export const STATE_PROPERTIES = [
|
|
66
|
+
'ref',
|
|
67
|
+
'parent',
|
|
68
|
+
'__element',
|
|
69
|
+
'__depends',
|
|
70
|
+
'__ref',
|
|
71
|
+
'__children',
|
|
72
|
+
'root'
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
export const STATE_METHODS = new Set([
|
|
76
|
+
'update',
|
|
77
|
+
'parse',
|
|
78
|
+
'clean',
|
|
79
|
+
'create',
|
|
80
|
+
'destroy',
|
|
81
|
+
'add',
|
|
82
|
+
'toggle',
|
|
83
|
+
'remove',
|
|
84
|
+
'apply',
|
|
85
|
+
'set',
|
|
86
|
+
'reset',
|
|
87
|
+
'replace',
|
|
88
|
+
'quietReplace',
|
|
89
|
+
'quietUpdate',
|
|
90
|
+
'applyReplace',
|
|
91
|
+
'applyFunction',
|
|
92
|
+
'keys',
|
|
93
|
+
'values',
|
|
94
|
+
'ref',
|
|
95
|
+
'rootUpdate',
|
|
96
|
+
'parentUpdate',
|
|
97
|
+
'parent',
|
|
98
|
+
'__element',
|
|
99
|
+
'__depends',
|
|
100
|
+
'__ref',
|
|
101
|
+
'__children',
|
|
102
|
+
'root',
|
|
103
|
+
'setByPath',
|
|
104
|
+
'setPathCollection',
|
|
105
|
+
'removeByPath',
|
|
106
|
+
'removePathCollection',
|
|
107
|
+
'getByPath'
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
export const PROPS_METHODS = new Set(['update', '__element'])
|
|
111
|
+
|
|
112
|
+
export const METHODS = new Set([
|
|
113
|
+
'set',
|
|
114
|
+
'reset',
|
|
115
|
+
'update',
|
|
116
|
+
'remove',
|
|
117
|
+
'updateContent',
|
|
118
|
+
'removeContent',
|
|
119
|
+
'lookup',
|
|
120
|
+
'lookdown',
|
|
121
|
+
'lookdownAll',
|
|
122
|
+
'getRef',
|
|
123
|
+
'getPath',
|
|
124
|
+
'setNodeStyles',
|
|
125
|
+
'spotByPath',
|
|
126
|
+
'keys',
|
|
127
|
+
'parse',
|
|
128
|
+
'setProps',
|
|
129
|
+
'parseDeep',
|
|
130
|
+
'variables',
|
|
131
|
+
'if',
|
|
132
|
+
'log',
|
|
133
|
+
'verbose',
|
|
134
|
+
'warn',
|
|
135
|
+
'error',
|
|
136
|
+
'call',
|
|
137
|
+
'nextElement',
|
|
138
|
+
'previousElement',
|
|
139
|
+
'getRootState',
|
|
140
|
+
'getRoot',
|
|
141
|
+
'getRootData',
|
|
142
|
+
'getRootContext',
|
|
143
|
+
'getContext',
|
|
144
|
+
'getQuery',
|
|
145
|
+
'getDB',
|
|
146
|
+
'getChildren',
|
|
147
|
+
'preventContentUpdate',
|
|
148
|
+
'preventContentRecreate'
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
export const METHODS_EXL = new Set([
|
|
152
|
+
'node', 'context', 'extends', '__element', '__ref',
|
|
153
|
+
...METHODS,
|
|
154
|
+
...STATE_METHODS,
|
|
155
|
+
...PROPS_METHODS
|
|
156
|
+
])
|
|
157
|
+
|
|
158
|
+
export const DOMQL_EVENTS = new Set([
|
|
159
|
+
'init',
|
|
160
|
+
'beforeClassAssign',
|
|
161
|
+
'render',
|
|
162
|
+
'renderRouter',
|
|
163
|
+
'attachNode',
|
|
164
|
+
'stateInit',
|
|
165
|
+
'stateCreated',
|
|
166
|
+
'beforeStateUpdate',
|
|
167
|
+
'stateUpdate',
|
|
168
|
+
'beforeUpdate',
|
|
169
|
+
'done',
|
|
170
|
+
'create',
|
|
171
|
+
'complete',
|
|
172
|
+
'frame',
|
|
173
|
+
'update',
|
|
174
|
+
'fetchError',
|
|
175
|
+
'fetchComplete'
|
|
176
|
+
])
|
package/log.js
ADDED
package/metadata.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import { isObject } from './types.js'
|
|
4
|
+
|
|
5
|
+
const escapeHtml = (text) => {
|
|
6
|
+
if (text === null || text === undefined) return ''
|
|
7
|
+
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
|
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
|
+
// For non-production (preview/staging), inject noindex into metadata
|
|
23
|
+
// but still generate full OG/Twitter tags for sharing previews
|
|
24
|
+
if (!isProduction) {
|
|
25
|
+
metadata = { ...metadata, robots: 'noindex' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tags = Object.entries(metadata).reduce(
|
|
29
|
+
(acc, [key, value]) => {
|
|
30
|
+
if (!value && value !== 0 && value !== false) return acc
|
|
31
|
+
|
|
32
|
+
if (key === 'title') {
|
|
33
|
+
acc.push(`<title>${escapeHtml(value)}</title>`)
|
|
34
|
+
return acc
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (key === 'canonical') {
|
|
38
|
+
acc.push(`<link rel="canonical" href="${escapeHtml(value)}">`)
|
|
39
|
+
return acc
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (key === 'manifest') {
|
|
43
|
+
acc.push(`<link rel="manifest" href="${escapeHtml(value)}">`)
|
|
44
|
+
return acc
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (key === 'favicon' || key === 'favicons' || key === 'icon' || key === 'icons') {
|
|
48
|
+
const items = Array.isArray(value) ? value : [value]
|
|
49
|
+
items.forEach((item) => {
|
|
50
|
+
if (typeof item === 'string') {
|
|
51
|
+
acc.push(`<link rel="icon" href="${escapeHtml(item)}">`)
|
|
52
|
+
} else if (typeof item === 'object') {
|
|
53
|
+
const attrs = buildAttrs(item)
|
|
54
|
+
if (!/rel=/.test(attrs)) {
|
|
55
|
+
acc.push(`<link rel="icon" ${attrs}>`)
|
|
56
|
+
} else {
|
|
57
|
+
acc.push(`<link ${attrs}>`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
return acc
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (key === 'alternate') {
|
|
65
|
+
const alternates = Array.isArray(value) ? value : [value]
|
|
66
|
+
alternates.forEach((alt) => {
|
|
67
|
+
if (typeof alt === 'object') {
|
|
68
|
+
const attrs = Object.entries(alt)
|
|
69
|
+
.filter(([_, attrValue]) => attrValue !== undefined)
|
|
70
|
+
.map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`)
|
|
71
|
+
.join(' ')
|
|
72
|
+
acc.push(`<link rel="alternate" ${attrs}>`)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
return acc
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const processMetaTag = (tagKey, tagValue, attrType = 'name') => {
|
|
79
|
+
if (
|
|
80
|
+
typeof tagValue === 'string' ||
|
|
81
|
+
typeof tagValue === 'number' ||
|
|
82
|
+
typeof tagValue === 'boolean'
|
|
83
|
+
) {
|
|
84
|
+
acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(tagValue)}">`)
|
|
85
|
+
} else if (Array.isArray(tagValue)) {
|
|
86
|
+
tagValue.forEach((item) => {
|
|
87
|
+
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
|
|
88
|
+
acc.push(`<meta ${attrType}="${escapeHtml(tagKey)}" content="${escapeHtml(item)}">`)
|
|
89
|
+
} else if (typeof item === 'object') {
|
|
90
|
+
const attrs = Object.entries(item)
|
|
91
|
+
.filter(([_, attrValue]) => attrValue !== undefined)
|
|
92
|
+
.map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`)
|
|
93
|
+
.join(' ')
|
|
94
|
+
acc.push(`<meta ${attrs}>`)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
} else if (typeof tagValue === 'object') {
|
|
98
|
+
const attrs = Object.entries(tagValue)
|
|
99
|
+
.filter(([_, attrValue]) => attrValue !== undefined)
|
|
100
|
+
.map(([attrKey, attrValue]) => `${attrKey}="${escapeHtml(attrValue)}"`)
|
|
101
|
+
.join(' ')
|
|
102
|
+
acc.push(`<meta ${attrs}>`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (key.startsWith('http-equiv:')) {
|
|
107
|
+
const httpEquivKey = key.replace('http-equiv:', '')
|
|
108
|
+
processMetaTag(httpEquivKey, value, 'http-equiv')
|
|
109
|
+
return acc
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (key.startsWith('itemprop:')) {
|
|
113
|
+
const itempropKey = key.replace('itemprop:', '')
|
|
114
|
+
processMetaTag(itempropKey, value, 'itemprop')
|
|
115
|
+
return acc
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const prefixes = [
|
|
119
|
+
'og:', 'twitter:', 'fb:', 'article:', 'profile:', 'book:',
|
|
120
|
+
'business:', 'music:', 'product:', 'video:', 'DC:', 'DCTERMS:'
|
|
121
|
+
]
|
|
122
|
+
const prefix = prefixes.find((p) => key.startsWith(p))
|
|
123
|
+
if (prefix) {
|
|
124
|
+
const tagKey = key.replace(prefix, '')
|
|
125
|
+
processMetaTag(
|
|
126
|
+
`${prefix.replace(':', '')}:${tagKey}`,
|
|
127
|
+
value,
|
|
128
|
+
prefix === 'twitter:' || prefix === 'DC:' || prefix === 'DCTERMS:' ? 'name' : 'property'
|
|
129
|
+
)
|
|
130
|
+
return acc
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (key.startsWith('apple:') || key.startsWith('msapplication:')) {
|
|
134
|
+
const pfx = key.startsWith('apple:') ? 'apple-' : 'msapplication-'
|
|
135
|
+
const tagKey = key.replace(/^apple:|^msapplication:/, '')
|
|
136
|
+
processMetaTag(`${pfx}${tagKey}`, value, 'name')
|
|
137
|
+
return acc
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
processMetaTag(key, value)
|
|
141
|
+
return acc
|
|
142
|
+
},
|
|
143
|
+
[
|
|
144
|
+
'<meta charset="UTF-8">',
|
|
145
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">'
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return tags.join('\n')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Extract page-level metadata from project data for a given pathname.
|
|
154
|
+
* Merges global SEO (data.integrations.seo) with page-level metadata/helmet/state.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* Check if a string looks like a bare filename (not an absolute path or URL).
|
|
158
|
+
* These are references to project files from `data.files`.
|
|
159
|
+
*/
|
|
160
|
+
const isBareFilename = (val) =>
|
|
161
|
+
typeof val === 'string' && val.length > 0 && !val.startsWith('/') && !val.startsWith('http')
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve bare filename references in metadata values against `data.files`.
|
|
165
|
+
* If a metadata value is a plain filename (e.g. "logo.png") and a matching
|
|
166
|
+
* entry exists in `data.files`, replace it with the file's `src` URL.
|
|
167
|
+
*/
|
|
168
|
+
function resolveFileReferences (metadata, files) {
|
|
169
|
+
if (!files || typeof files !== 'object') return metadata
|
|
170
|
+
|
|
171
|
+
const resolve = (val) => {
|
|
172
|
+
if (!isBareFilename(val)) return val
|
|
173
|
+
const fileEntry = files[val]
|
|
174
|
+
if (fileEntry?.src) return fileEntry.src
|
|
175
|
+
return val
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const result = { ...metadata }
|
|
179
|
+
for (const [key, value] of Object.entries(result)) {
|
|
180
|
+
if (typeof value === 'string') {
|
|
181
|
+
result[key] = resolve(value)
|
|
182
|
+
} else if (Array.isArray(value)) {
|
|
183
|
+
result[key] = value.map((item) =>
|
|
184
|
+
typeof item === 'string' ? resolve(item) : item
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolve a dot-path like "item.title_ka" against an object.
|
|
193
|
+
*/
|
|
194
|
+
function resolveDotPath (obj, path) {
|
|
195
|
+
if (!obj || !path) return undefined
|
|
196
|
+
const parts = path.split('.')
|
|
197
|
+
let v = obj
|
|
198
|
+
for (const p of parts) {
|
|
199
|
+
if (v == null || typeof v !== 'object') return undefined
|
|
200
|
+
v = v[p]
|
|
201
|
+
}
|
|
202
|
+
return v
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve {{ key | polyglot }} and {{ key | getLocalStateLang }} templates
|
|
207
|
+
* in metadata string values using project polyglot translations and SSR state.
|
|
208
|
+
*/
|
|
209
|
+
function resolveMetadataTemplates (metadata, data, ssrContext) {
|
|
210
|
+
const config = data.config || data.settings || {}
|
|
211
|
+
const polyglot = config.polyglot || data.polyglot
|
|
212
|
+
const defaultLang = ssrContext?.lang || polyglot?.defaultLang || 'en'
|
|
213
|
+
const translations = polyglot?.translations || {}
|
|
214
|
+
const langMap = translations[defaultLang] || {}
|
|
215
|
+
const state = ssrContext?.state || {}
|
|
216
|
+
|
|
217
|
+
const result = { ...metadata }
|
|
218
|
+
for (const [key, value] of Object.entries(result)) {
|
|
219
|
+
if (typeof value !== 'string' || !value.includes('{{')) continue
|
|
220
|
+
|
|
221
|
+
result[key] = value.replace(/\{\{\s*([^|{}]+?)\s*(?:\|\s*(\w+)\s*)?\}\}/g, (match, k, filter) => {
|
|
222
|
+
const trimmed = k.trim()
|
|
223
|
+
|
|
224
|
+
if (filter === 'polyglot') {
|
|
225
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (filter === 'getLocalStateLang') {
|
|
229
|
+
// key is like "item.title_" — append lang
|
|
230
|
+
const resolved = resolveDotPath(state, trimmed + defaultLang)
|
|
231
|
+
return resolved ?? match
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// No filter — try state lookup, then polyglot
|
|
235
|
+
const fromState = resolveDotPath(state, trimmed)
|
|
236
|
+
if (fromState !== undefined) return fromState
|
|
237
|
+
|
|
238
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
return result
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Convert function-string metadata values to {{ }} templates.
|
|
246
|
+
* Handles common patterns from Symbols page definitions:
|
|
247
|
+
* (el, s) => s.item ? el.call("getLocalStateLang", "item.title_") : ""
|
|
248
|
+
* (el, s) => s.item ? s.item.image_url : ""
|
|
249
|
+
*/
|
|
250
|
+
function convertFunctionMetaToTemplates (metadata) {
|
|
251
|
+
const result = { ...metadata }
|
|
252
|
+
for (const [key, value] of Object.entries(result)) {
|
|
253
|
+
if (typeof value === 'function') {
|
|
254
|
+
const src = value.toString()
|
|
255
|
+
// Pattern: el.call("getLocalStateLang", "item.title_")
|
|
256
|
+
const langMatch = src.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/)
|
|
257
|
+
if (langMatch) {
|
|
258
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
// Pattern: el.call("polyglot", "item.description")
|
|
262
|
+
const polyMatch = src.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/)
|
|
263
|
+
if (polyMatch) {
|
|
264
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
// Pattern: s.item.field or s.item?.field
|
|
268
|
+
const stateMatch = src.match(/s\.(\w+(?:\.\w+)+)/)
|
|
269
|
+
if (stateMatch) {
|
|
270
|
+
result[key] = `{{ ${stateMatch[1]} }}`
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
// Could not parse — drop function value
|
|
274
|
+
delete result[key]
|
|
275
|
+
} else if (typeof value === 'string') {
|
|
276
|
+
// Function-string (not yet destringified)
|
|
277
|
+
const langMatch = value.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/)
|
|
278
|
+
if (langMatch) {
|
|
279
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
const polyMatch = value.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/)
|
|
283
|
+
if (polyMatch) {
|
|
284
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
// Function-string with s.item.field
|
|
288
|
+
if (value.includes('=>') && value.includes('s.')) {
|
|
289
|
+
const stateMatch = value.match(/s\.(\w+(?:\.\w+)+)/)
|
|
290
|
+
if (stateMatch) {
|
|
291
|
+
result[key] = `{{ ${stateMatch[1]} }}`
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return result
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Extract page-level metadata from project data for a given pathname.
|
|
302
|
+
* @param {object} data - Full project data
|
|
303
|
+
* @param {string} pathname - Route path (e.g. '/blog/:id')
|
|
304
|
+
* @param {object} [ssrContext] - SSR context for resolving dynamic metadata
|
|
305
|
+
* @param {object} [ssrContext.state] - Page state with prefetched data
|
|
306
|
+
* @param {string} [ssrContext.lang] - Active language code (e.g. 'ka')
|
|
307
|
+
* @param {string} [ssrContext.actualPathname] - Actual URL pathname (e.g. '/blog/abc-123')
|
|
308
|
+
*/
|
|
309
|
+
export function getPageMetadata (data, pathname, ssrContext, { isProduction = false, channel = 'production' } = {}) {
|
|
310
|
+
const currentPage = data.pages?.[pathname]
|
|
311
|
+
const stateObject = isObject(currentPage?.state) && currentPage?.state
|
|
312
|
+
const rawPageMeta = currentPage?.metadata || currentPage?.helmet || stateObject || {}
|
|
313
|
+
|
|
314
|
+
// Convert function values to {{ }} templates instead of filtering them out
|
|
315
|
+
const pageMetadata = convertFunctionMetaToTemplates(rawPageMeta)
|
|
316
|
+
|
|
317
|
+
// Track which keys the page explicitly sets
|
|
318
|
+
const pageExplicitKeys = new Set(Object.keys(pageMetadata))
|
|
319
|
+
|
|
320
|
+
const appMetadata = data.app?.metadata || {}
|
|
321
|
+
const cleanAppMeta = convertFunctionMetaToTemplates(appMetadata)
|
|
322
|
+
|
|
323
|
+
let merged = {}
|
|
324
|
+
if (data.integrations?.seo) {
|
|
325
|
+
merged = { ...data.integrations.seo, ...cleanAppMeta, ...pageMetadata }
|
|
326
|
+
} else if (Object.keys(cleanAppMeta).length) {
|
|
327
|
+
merged = { ...cleanAppMeta, ...pageMetadata }
|
|
328
|
+
} else {
|
|
329
|
+
merged = { ...pageMetadata }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!merged.title) {
|
|
333
|
+
const label = data.name || data.key
|
|
334
|
+
if (label) {
|
|
335
|
+
if (isProduction) merged.title = label
|
|
336
|
+
else if (channel === 'development') merged.title = `${label} / dev:symbo.ls`
|
|
337
|
+
else if (channel === 'staging') merged.title = `${label} / staging:symbo.ls`
|
|
338
|
+
else merged.title = `${label} / symbo.ls`
|
|
339
|
+
} else {
|
|
340
|
+
merged.title = 'Symbols demo'
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Resolve {{ }} templates in metadata values (polyglot, state, getLocalStateLang)
|
|
345
|
+
merged = resolveMetadataTemplates(merged, data, ssrContext)
|
|
346
|
+
|
|
347
|
+
// Auto-cascade page-level title/description to OG/Twitter tags
|
|
348
|
+
if (merged.title && (pageExplicitKeys.has('title') || !merged['og:title'])) {
|
|
349
|
+
if (!pageExplicitKeys.has('og:title')) {
|
|
350
|
+
merged['og:title'] = merged.title
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (merged.description && (pageExplicitKeys.has('description') || !merged['og:description'])) {
|
|
354
|
+
if (!pageExplicitKeys.has('og:description')) {
|
|
355
|
+
merged['og:description'] = merged.description
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (merged.title && !merged['twitter:title']) {
|
|
359
|
+
merged['twitter:title'] = merged.title
|
|
360
|
+
}
|
|
361
|
+
if (merged.description && !merged['twitter:description']) {
|
|
362
|
+
merged['twitter:description'] = merged.description
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Make og:url route-aware — use actual pathname if available
|
|
366
|
+
const routeForUrl = ssrContext?.actualPathname || pathname
|
|
367
|
+
if (merged['og:url'] || merged.url) {
|
|
368
|
+
const baseUrl = (merged['og:url'] || merged.url || '').replace(/\/$/, '')
|
|
369
|
+
if (baseUrl && routeForUrl && routeForUrl !== '/') {
|
|
370
|
+
const cleanRoute = routeForUrl.startsWith('/') ? routeForUrl : '/' + routeForUrl
|
|
371
|
+
merged['og:url'] = baseUrl + cleanRoute
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
merged = resolveFileReferences(merged, data.files)
|
|
376
|
+
|
|
377
|
+
// Clean up any unresolved {{ }} templates (when prefetch fails or returns no data)
|
|
378
|
+
const siteName = data.name || data.app?.metadata?.title || data.app?.name || ''
|
|
379
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
380
|
+
if (typeof value === 'string' && value.includes('{{')) {
|
|
381
|
+
const cleaned = value.replace(/\{\{[^}]*\}\}/g, '').trim()
|
|
382
|
+
if (!cleaned) {
|
|
383
|
+
if (key === 'title') merged[key] = siteName
|
|
384
|
+
else delete merged[key]
|
|
385
|
+
} else {
|
|
386
|
+
merged[key] = cleaned
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return merged
|
|
392
|
+
}
|