assign-gingerly 0.0.53 → 0.0.55
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/README.md +847 -5
- package/assignFrom.js +229 -9
- package/assignFrom.ts +338 -9
- package/assignGingerly.js +34 -1
- package/assignGingerly.ts +54 -1
- package/beVigilant.js +73 -0
- package/beVigilant.ts +85 -0
- package/enhanceAll.js +106 -0
- package/enhanceAll.ts +138 -0
- package/handlers/join.js +74 -0
- package/handlers/join.ts +80 -0
- package/handlers/lazyLoad.js +212 -0
- package/handlers/lazyLoad.ts +307 -0
- package/handlers/lazyLoadSwitch.js +58 -0
- package/handlers/lazyLoadSwitch.ts +63 -0
- package/handlers/microDataJoin.js +184 -0
- package/handlers/microDataJoin.ts +270 -0
- package/inferencer/.gitmodules +3 -0
- package/inferencer/.vscode/settings.json +2 -0
- package/inferencer/InferencedPropagator.js +230 -0
- package/inferencer/InferencedPropagator.ts +269 -0
- package/inferencer/LICENSE +21 -0
- package/inferencer/README.md +524 -0
- package/inferencer/Requirements/SupportForPropagator.md +368 -0
- package/inferencer/imports.html +7 -0
- package/inferencer/inferencer.js +254 -0
- package/inferencer/inferencer.ts +292 -0
- package/inferencer/package-lock.json +129 -0
- package/inferencer/package.json +60 -0
- package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
- package/inferencer/playwright-report/index.html +90 -0
- package/inferencer/playwright.config.ts +54 -0
- package/inferencer/test-results/.last-run.json +6 -0
- package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
- package/inferencer/tests/inferencedPropagator.html +428 -0
- package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
- package/inferencer/tests/inferencer.html +355 -0
- package/inferencer/tests/inferencer.spec.ts +19 -0
- package/inferencer/tsconfig.json +19 -0
- package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
- package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
- package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
- package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
- package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
- package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
- package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
- package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
- package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
- package/inferencer/types/LICENSE +21 -0
- package/inferencer/types/NewCustomElement.md +388 -0
- package/inferencer/types/NewCustomElementFeature.md +683 -0
- package/inferencer/types/NewEnhancementInstructions.md +705 -0
- package/inferencer/types/README.md +2 -0
- package/inferencer/types/agrace/types.d.ts +11 -0
- package/inferencer/types/assign-gingerly/types.d.ts +572 -0
- package/inferencer/types/be-a-beacon/types.d.ts +17 -0
- package/inferencer/types/be-bound/types.d.ts +66 -0
- package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
- package/inferencer/types/be-calculating/types.d.ts +54 -0
- package/inferencer/types/be-clonable/types.d.ts +38 -0
- package/inferencer/types/be-committed/types.d.ts +22 -0
- package/inferencer/types/be-consoling/types.d.ts +24 -0
- package/inferencer/types/be-decked-with/types.d.ts +26 -0
- package/inferencer/types/be-delible/types.d.ts +27 -0
- package/inferencer/types/be-dispatching/types.d.ts +34 -0
- package/inferencer/types/be-evanescent/types.d.ts +20 -0
- package/inferencer/types/be-flashy/types.d.ts +21 -0
- package/inferencer/types/be-gone/types.d.ts +25 -0
- package/inferencer/types/be-observing/types.d.ts +55 -0
- package/inferencer/types/be-reflective/types.d.ts +78 -0
- package/inferencer/types/be-reformable/types.d.ts +49 -0
- package/inferencer/types/be-render-neutral/types.d.ts +32 -0
- package/inferencer/types/be-switched/types.d.ts +146 -0
- package/inferencer/types/be-typed/types.d.ts +32 -0
- package/inferencer/types/be-valued/types.d.ts +22 -0
- package/inferencer/types/data-props/types.d.ts +34 -0
- package/inferencer/types/do-inc/types.d.ts +56 -0
- package/inferencer/types/do-invoke/types.d.ts +38 -0
- package/inferencer/types/do-merge/types.d.ts +28 -0
- package/inferencer/types/do-toggle/types.d.ts +31 -0
- package/inferencer/types/face-up/types.d.ts +100 -0
- package/inferencer/types/fetch-for/types.d.ts +36 -0
- package/inferencer/types/folder-picker/types.d.ts +21 -0
- package/inferencer/types/global.d.ts +29 -0
- package/inferencer/types/id-generation/types.d.ts +26 -0
- package/inferencer/types/inferencer/types.d.ts +46 -0
- package/inferencer/types/mount-observer/types.d.ts +363 -0
- package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
- package/inferencer/types/pipe-in/types.d.ts +52 -0
- package/inferencer/types/roundabout/types.d.ts +268 -0
- package/inferencer/types/soak-up/types.d.ts +40 -0
- package/inferencer/types/templ-maker/types.d.ts +43 -0
- package/inferencer/types/time-ticker/types.d.ts +62 -0
- package/inferencer/types/truth-sourcer/types.d.ts +44 -0
- package/inferencer/upSearch.js +27 -0
- package/inferencer/upSearch.ts +26 -0
- package/inferencer/withScopePerimeter.js +27 -0
- package/inferencer/withScopePerimeter.ts +33 -0
- package/inferredAssignments.js +38 -0
- package/inferredAssignments.ts +65 -0
- package/isAllowedImportPath.js +42 -0
- package/isAllowedImportPath.ts +53 -0
- package/package.json +57 -3
- package/paths.js +231 -0
- package/paths.ts +413 -0
- package/processHandlerCommands.js +188 -0
- package/processHandlerCommands.ts +217 -0
- package/resolveIdRef.js +144 -0
- package/resolveIdRef.ts +170 -0
- package/resolveValues.js +41 -2
- package/resolveValues.ts +41 -1
- package/transitionHelper.js +109 -0
- package/transitionHelper.ts +132 -0
- package/types/assign-gingerly/types.d.ts +89 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.microDataJoin handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Renders a template array as semantic microdata-annotated DOM elements.
|
|
5
|
+
* Each dynamic value gets an HTML element based on its resolved type,
|
|
6
|
+
* with itemprop set to the property name.
|
|
7
|
+
*
|
|
8
|
+
* This handler is auto-loaded by processHandlerCommands when
|
|
9
|
+
* `do: 'builtIns.microDataJoin'` is encountered.
|
|
10
|
+
*/
|
|
11
|
+
const MARKER_START_PREFIX = '?start name="';
|
|
12
|
+
const MARKER_END = '?end';
|
|
13
|
+
const MARKER_NAME = 'microDataJoin';
|
|
14
|
+
|
|
15
|
+
function findMarkers(target) {
|
|
16
|
+
const startText = `${MARKER_START_PREFIX}${MARKER_NAME}"`;
|
|
17
|
+
let startMarker = null;
|
|
18
|
+
let endMarker = null;
|
|
19
|
+
const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
|
|
20
|
+
let node;
|
|
21
|
+
while ((node = walker.nextNode())) {
|
|
22
|
+
if (!startMarker && node.data === startText) {
|
|
23
|
+
startMarker = node;
|
|
24
|
+
} else if (startMarker && !endMarker && node.data === MARKER_END) {
|
|
25
|
+
endMarker = node;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [startMarker, endMarker];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createMarkers(target) {
|
|
33
|
+
const startMarker = document.createComment(`${MARKER_START_PREFIX}${MARKER_NAME}"`);
|
|
34
|
+
const endMarker = document.createComment(MARKER_END);
|
|
35
|
+
target.appendChild(startMarker);
|
|
36
|
+
target.appendChild(endMarker);
|
|
37
|
+
return [startMarker, endMarker];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getNodesBetweenMarkers(start, end) {
|
|
41
|
+
const nodes = [];
|
|
42
|
+
let current = start.nextSibling;
|
|
43
|
+
while (current && current !== end) {
|
|
44
|
+
nodes.push(current);
|
|
45
|
+
current = current.nextSibling;
|
|
46
|
+
}
|
|
47
|
+
return nodes;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatForDisplay(value) {
|
|
51
|
+
if (value instanceof Date) {
|
|
52
|
+
return value.toLocaleDateString();
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'number') {
|
|
55
|
+
return value.toLocaleString();
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === 'boolean') {
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
return String(value ?? '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatForAttribute(value) {
|
|
64
|
+
if (value instanceof Date) {
|
|
65
|
+
return value.toISOString();
|
|
66
|
+
}
|
|
67
|
+
return String(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createElementForValue(prop, value) {
|
|
71
|
+
if (value instanceof Date) {
|
|
72
|
+
const el = document.createElement('time');
|
|
73
|
+
el.setAttribute('itemprop', prop);
|
|
74
|
+
el.setAttribute('datetime', formatForAttribute(value));
|
|
75
|
+
el.textContent = formatForDisplay(value);
|
|
76
|
+
return el;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === 'number') {
|
|
79
|
+
const el = document.createElement('data');
|
|
80
|
+
el.setAttribute('itemprop', prop);
|
|
81
|
+
el.setAttribute('value', formatForAttribute(value));
|
|
82
|
+
el.textContent = formatForDisplay(value);
|
|
83
|
+
return el;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === 'boolean') {
|
|
86
|
+
const el = document.createElement('data');
|
|
87
|
+
el.setAttribute('itemprop', prop);
|
|
88
|
+
el.setAttribute('value', String(value));
|
|
89
|
+
return el;
|
|
90
|
+
}
|
|
91
|
+
const el = document.createElement('span');
|
|
92
|
+
el.setAttribute('itemprop', prop);
|
|
93
|
+
el.textContent = String(value ?? '');
|
|
94
|
+
return el;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function updateElement(el, prop, value) {
|
|
98
|
+
if (value instanceof Date) {
|
|
99
|
+
el.setAttribute('datetime', formatForAttribute(value));
|
|
100
|
+
el.textContent = formatForDisplay(value);
|
|
101
|
+
} else if (typeof value === 'number') {
|
|
102
|
+
el.setAttribute('value', formatForAttribute(value));
|
|
103
|
+
el.textContent = formatForDisplay(value);
|
|
104
|
+
} else if (typeof value === 'boolean') {
|
|
105
|
+
el.setAttribute('value', String(value));
|
|
106
|
+
} else {
|
|
107
|
+
el.textContent = String(value ?? '');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function processTemplate(template) {
|
|
112
|
+
const result = [];
|
|
113
|
+
for (const item of template) {
|
|
114
|
+
if (Array.isArray(item)) {
|
|
115
|
+
const hasNull = item.some(el =>
|
|
116
|
+
el && typeof el === 'object' && 'prop' in el && el.val == null
|
|
117
|
+
);
|
|
118
|
+
if (hasNull) continue;
|
|
119
|
+
result.push(...processTemplate(item));
|
|
120
|
+
} else if (item == null) {
|
|
121
|
+
continue;
|
|
122
|
+
} else {
|
|
123
|
+
result.push(item);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class MicroDataJoinHandler {
|
|
130
|
+
config;
|
|
131
|
+
constructor(config) {
|
|
132
|
+
this.config = config;
|
|
133
|
+
}
|
|
134
|
+
async assign(lhsTarget, resolvedParams) {
|
|
135
|
+
const { template } = resolvedParams;
|
|
136
|
+
if (!template || !Array.isArray(template)) {
|
|
137
|
+
throw new Error('builtIns.microDataJoin: resolve.template must be an array');
|
|
138
|
+
}
|
|
139
|
+
if (!(lhsTarget instanceof Element)) {
|
|
140
|
+
throw new Error('builtIns.microDataJoin: lhsTarget must be a DOM Element');
|
|
141
|
+
}
|
|
142
|
+
if (!lhsTarget.hasAttribute('itemscope')) {
|
|
143
|
+
lhsTarget.setAttribute('itemscope', '');
|
|
144
|
+
}
|
|
145
|
+
const processed = processTemplate(template);
|
|
146
|
+
let [startMarker, endMarker] = findMarkers(lhsTarget);
|
|
147
|
+
const isUpdate = startMarker !== null && endMarker !== null;
|
|
148
|
+
if (!isUpdate) {
|
|
149
|
+
[startMarker, endMarker] = createMarkers(lhsTarget);
|
|
150
|
+
}
|
|
151
|
+
if (isUpdate) {
|
|
152
|
+
const existingNodes = getNodesBetweenMarkers(startMarker, endMarker);
|
|
153
|
+
let nodeIdx = 0;
|
|
154
|
+
for (const segment of processed) {
|
|
155
|
+
if (typeof segment === 'string') {
|
|
156
|
+
const node = existingNodes[nodeIdx];
|
|
157
|
+
if (node && node.nodeType === Node.TEXT_NODE) {
|
|
158
|
+
if (node.textContent !== segment) {
|
|
159
|
+
node.textContent = segment;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
nodeIdx++;
|
|
163
|
+
} else {
|
|
164
|
+
const node = existingNodes[nodeIdx];
|
|
165
|
+
if (node && node instanceof Element && node.getAttribute('itemprop') === segment.prop) {
|
|
166
|
+
updateElement(node, segment.prop, segment.val);
|
|
167
|
+
}
|
|
168
|
+
nodeIdx++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
const fragment = document.createDocumentFragment();
|
|
173
|
+
for (const segment of processed) {
|
|
174
|
+
if (typeof segment === 'string') {
|
|
175
|
+
fragment.appendChild(document.createTextNode(segment));
|
|
176
|
+
} else {
|
|
177
|
+
const el = createElementForValue(segment.prop, segment.val);
|
|
178
|
+
fragment.appendChild(el);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
endMarker.parentNode.insertBefore(fragment, endMarker);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.microDataJoin handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Renders a template array as semantic microdata-annotated DOM elements.
|
|
5
|
+
* Each dynamic value gets an HTML element based on its resolved type,
|
|
6
|
+
* with itemprop set to the property name.
|
|
7
|
+
*
|
|
8
|
+
* Type → Element mapping:
|
|
9
|
+
* - string → <span itemprop=name>value</span>
|
|
10
|
+
* - number → <data itemprop=name value=raw>formatted</data>
|
|
11
|
+
* - boolean → <data itemprop=name value=true/false></data>
|
|
12
|
+
* - Date → <time itemprop=name datetime=iso>formatted</time>
|
|
13
|
+
* - null/undefined → omitted (or sub-array dropped if in optional segment)
|
|
14
|
+
*
|
|
15
|
+
* Uses comment markers for idempotent updates — first call creates elements,
|
|
16
|
+
* subsequent calls update existing elements in place.
|
|
17
|
+
*
|
|
18
|
+
* This handler is auto-loaded by processHandlerCommands when
|
|
19
|
+
* `do: 'builtIns.microDataJoin'` is encountered.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* assignFrom(oSection, {
|
|
23
|
+
* '?.querySelector?.div =>': {
|
|
24
|
+
* do: 'builtIns.microDataJoin',
|
|
25
|
+
* resolve: {
|
|
26
|
+
* template: [
|
|
27
|
+
* { prop: 'firstName', val: '?.firstName' },
|
|
28
|
+
* ' ',
|
|
29
|
+
* { prop: 'lastName', val: '?.lastName' }
|
|
30
|
+
* ]
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* }, { from: vm, withMethods: ['querySelector'] });
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { AssignFromHandler } from '../assignFrom.js';
|
|
37
|
+
|
|
38
|
+
const MARKER_START_PREFIX = '?start name="';
|
|
39
|
+
const MARKER_END = '?end';
|
|
40
|
+
const MARKER_NAME = 'microDataJoin';
|
|
41
|
+
|
|
42
|
+
interface TemplateSegment {
|
|
43
|
+
prop: string;
|
|
44
|
+
val: any;
|
|
45
|
+
format?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find existing start/end comment markers in a target element.
|
|
50
|
+
*/
|
|
51
|
+
function findMarkers(target: Element): [Comment | null, Comment | null] {
|
|
52
|
+
const startText = `${MARKER_START_PREFIX}${MARKER_NAME}"`;
|
|
53
|
+
let startMarker: Comment | null = null;
|
|
54
|
+
let endMarker: Comment | null = null;
|
|
55
|
+
|
|
56
|
+
const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
|
|
57
|
+
let node: Comment | null;
|
|
58
|
+
while ((node = walker.nextNode() as Comment | null)) {
|
|
59
|
+
if (!startMarker && node.data === startText) {
|
|
60
|
+
startMarker = node;
|
|
61
|
+
} else if (startMarker && !endMarker && node.data === MARKER_END) {
|
|
62
|
+
endMarker = node;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return [startMarker, endMarker];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create start/end markers in the target.
|
|
71
|
+
*/
|
|
72
|
+
function createMarkers(target: Element): [Comment, Comment] {
|
|
73
|
+
const startMarker = document.createComment(`${MARKER_START_PREFIX}${MARKER_NAME}"`);
|
|
74
|
+
const endMarker = document.createComment(MARKER_END);
|
|
75
|
+
target.appendChild(startMarker);
|
|
76
|
+
target.appendChild(endMarker);
|
|
77
|
+
return [startMarker, endMarker];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get all nodes between start and end markers.
|
|
82
|
+
*/
|
|
83
|
+
function getNodesBetweenMarkers(start: Comment, end: Comment): Node[] {
|
|
84
|
+
const nodes: Node[] = [];
|
|
85
|
+
let current: Node | null = start.nextSibling;
|
|
86
|
+
while (current && current !== end) {
|
|
87
|
+
nodes.push(current);
|
|
88
|
+
current = current.nextSibling;
|
|
89
|
+
}
|
|
90
|
+
return nodes;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format a value for display (textContent) using locale.
|
|
95
|
+
*/
|
|
96
|
+
function formatForDisplay(value: any): string {
|
|
97
|
+
if (value instanceof Date) {
|
|
98
|
+
return value.toLocaleDateString();
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === 'number') {
|
|
101
|
+
return value.toLocaleString();
|
|
102
|
+
}
|
|
103
|
+
if (typeof value === 'boolean') {
|
|
104
|
+
return ''; // booleans show nothing by default
|
|
105
|
+
}
|
|
106
|
+
return String(value ?? '');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format a value for the machine-readable attribute (value or datetime).
|
|
111
|
+
*/
|
|
112
|
+
function formatForAttribute(value: any): string {
|
|
113
|
+
if (value instanceof Date) {
|
|
114
|
+
return value.toISOString();
|
|
115
|
+
}
|
|
116
|
+
return String(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create the appropriate DOM element for a resolved value.
|
|
121
|
+
*/
|
|
122
|
+
function createElementForValue(prop: string, value: any): Element {
|
|
123
|
+
if (value instanceof Date) {
|
|
124
|
+
const el = document.createElement('time');
|
|
125
|
+
el.setAttribute('itemprop', prop);
|
|
126
|
+
el.setAttribute('datetime', formatForAttribute(value));
|
|
127
|
+
el.textContent = formatForDisplay(value);
|
|
128
|
+
return el;
|
|
129
|
+
}
|
|
130
|
+
if (typeof value === 'number') {
|
|
131
|
+
const el = document.createElement('data');
|
|
132
|
+
el.setAttribute('itemprop', prop);
|
|
133
|
+
el.setAttribute('value', formatForAttribute(value));
|
|
134
|
+
el.textContent = formatForDisplay(value);
|
|
135
|
+
return el;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === 'boolean') {
|
|
138
|
+
const el = document.createElement('data');
|
|
139
|
+
el.setAttribute('itemprop', prop);
|
|
140
|
+
el.setAttribute('value', String(value));
|
|
141
|
+
// empty textContent for booleans
|
|
142
|
+
return el;
|
|
143
|
+
}
|
|
144
|
+
// Default: string → span
|
|
145
|
+
const el = document.createElement('span');
|
|
146
|
+
el.setAttribute('itemprop', prop);
|
|
147
|
+
el.textContent = String(value ?? '');
|
|
148
|
+
return el;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Update an existing element with a new value (in-place update).
|
|
153
|
+
*/
|
|
154
|
+
function updateElement(el: Element, prop: string, value: any): void {
|
|
155
|
+
if (value instanceof Date) {
|
|
156
|
+
el.setAttribute('datetime', formatForAttribute(value));
|
|
157
|
+
el.textContent = formatForDisplay(value);
|
|
158
|
+
} else if (typeof value === 'number') {
|
|
159
|
+
el.setAttribute('value', formatForAttribute(value));
|
|
160
|
+
el.textContent = formatForDisplay(value);
|
|
161
|
+
} else if (typeof value === 'boolean') {
|
|
162
|
+
el.setAttribute('value', String(value));
|
|
163
|
+
} else {
|
|
164
|
+
el.textContent = String(value ?? '');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Process the template array, handling optional (nested) segments.
|
|
170
|
+
* Returns a flat array of items to render, with null sub-arrays dropped.
|
|
171
|
+
*/
|
|
172
|
+
function processTemplate(template: any[]): (string | TemplateSegment)[] {
|
|
173
|
+
const result: (string | TemplateSegment)[] = [];
|
|
174
|
+
for (const item of template) {
|
|
175
|
+
if (Array.isArray(item)) {
|
|
176
|
+
// All-or-nothing: if any {prop,val} segment has val === null/undefined, drop entire sub-array
|
|
177
|
+
const hasNull = item.some(el =>
|
|
178
|
+
el && typeof el === 'object' && 'prop' in el && el.val == null
|
|
179
|
+
);
|
|
180
|
+
if (hasNull) continue;
|
|
181
|
+
// Sub-array passes — flatten
|
|
182
|
+
result.push(...processTemplate(item));
|
|
183
|
+
} else if (item == null) {
|
|
184
|
+
continue;
|
|
185
|
+
} else {
|
|
186
|
+
result.push(item);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* MicroDataJoinHandler — renders template arrays as semantic microdata DOM.
|
|
194
|
+
*/
|
|
195
|
+
export class MicroDataJoinHandler implements AssignFromHandler {
|
|
196
|
+
config: any;
|
|
197
|
+
|
|
198
|
+
constructor(config: any) {
|
|
199
|
+
this.config = config;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async assign(lhsTarget: any, resolvedParams: Record<string, any>): Promise<void> {
|
|
203
|
+
const { template } = resolvedParams;
|
|
204
|
+
|
|
205
|
+
if (!template || !Array.isArray(template)) {
|
|
206
|
+
throw new Error('builtIns.microDataJoin: resolve.template must be an array');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!(lhsTarget instanceof Element)) {
|
|
210
|
+
throw new Error('builtIns.microDataJoin: lhsTarget must be a DOM Element');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Add itemscope to target if not present
|
|
214
|
+
if (!lhsTarget.hasAttribute('itemscope')) {
|
|
215
|
+
lhsTarget.setAttribute('itemscope', '');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Process template (handle optional segments)
|
|
219
|
+
const processed = processTemplate(template);
|
|
220
|
+
|
|
221
|
+
// Find or create markers
|
|
222
|
+
let [startMarker, endMarker] = findMarkers(lhsTarget);
|
|
223
|
+
const isUpdate = startMarker !== null && endMarker !== null;
|
|
224
|
+
|
|
225
|
+
if (!isUpdate) {
|
|
226
|
+
[startMarker, endMarker] = createMarkers(lhsTarget);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (isUpdate) {
|
|
230
|
+
// Update existing nodes in place
|
|
231
|
+
const existingNodes = getNodesBetweenMarkers(startMarker!, endMarker!);
|
|
232
|
+
let nodeIdx = 0;
|
|
233
|
+
|
|
234
|
+
for (const segment of processed) {
|
|
235
|
+
if (typeof segment === 'string') {
|
|
236
|
+
// Literal text — update or skip text node
|
|
237
|
+
const node = existingNodes[nodeIdx];
|
|
238
|
+
if (node && node.nodeType === Node.TEXT_NODE) {
|
|
239
|
+
if (node.textContent !== segment) {
|
|
240
|
+
node.textContent = segment;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
nodeIdx++;
|
|
244
|
+
} else {
|
|
245
|
+
// {prop, val} — update element
|
|
246
|
+
const node = existingNodes[nodeIdx];
|
|
247
|
+
if (node && node instanceof Element && node.getAttribute('itemprop') === segment.prop) {
|
|
248
|
+
updateElement(node, segment.prop, segment.val);
|
|
249
|
+
}
|
|
250
|
+
nodeIdx++;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
// First render — create all nodes
|
|
255
|
+
const fragment = document.createDocumentFragment();
|
|
256
|
+
|
|
257
|
+
for (const segment of processed) {
|
|
258
|
+
if (typeof segment === 'string') {
|
|
259
|
+
fragment.appendChild(document.createTextNode(segment));
|
|
260
|
+
} else {
|
|
261
|
+
// {prop, val} object
|
|
262
|
+
const el = createElementForValue(segment.prop, segment.val);
|
|
263
|
+
fragment.appendChild(el);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
endMarker!.parentNode!.insertBefore(fragment, endMarker!);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { inferEventType } from './inferencer.js';
|
|
2
|
+
/**
|
|
3
|
+
* An inferred propagator that extends EventTarget.
|
|
4
|
+
* Emits property change events by intelligently hooking into
|
|
5
|
+
* whatever change detection mechanism is available for the element.
|
|
6
|
+
*
|
|
7
|
+
* Property observation is lazy — wiring only happens when
|
|
8
|
+
* addEventListener is called for a given property name.
|
|
9
|
+
*/
|
|
10
|
+
export class InferencedPropagator extends EventTarget {
|
|
11
|
+
#infer;
|
|
12
|
+
#watchedProperties = new Map();
|
|
13
|
+
constructor(infer) {
|
|
14
|
+
super();
|
|
15
|
+
this.#infer = infer;
|
|
16
|
+
}
|
|
17
|
+
addEventListener(type, callback, options) {
|
|
18
|
+
super.addEventListener(type, callback, options);
|
|
19
|
+
if (!this.#watchedProperties.has(type)) {
|
|
20
|
+
// Placeholder cleanup until async wiring completes
|
|
21
|
+
this.#watchedProperties.set(type, () => { });
|
|
22
|
+
this.#wireProperty(type);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
removeEventListener(type, callback, options) {
|
|
26
|
+
super.removeEventListener(type, callback, options);
|
|
27
|
+
}
|
|
28
|
+
async #wireProperty(propName) {
|
|
29
|
+
const element = this.#infer.enhancedElement;
|
|
30
|
+
// Snapshot current value before async setup
|
|
31
|
+
const initialValue = element[propName];
|
|
32
|
+
const cleanup = await this.#setupStrategy(element, propName);
|
|
33
|
+
this.#watchedProperties.set(propName, cleanup);
|
|
34
|
+
// If value changed during async wiring, dispatch immediately
|
|
35
|
+
const currentValue = element[propName];
|
|
36
|
+
if (currentValue !== initialValue) {
|
|
37
|
+
this.dispatchEvent(new Event(propName));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Determine and set up the best observation strategy for a property.
|
|
42
|
+
* Returns a cleanup function.
|
|
43
|
+
*/
|
|
44
|
+
async #setupStrategy(element, propName) {
|
|
45
|
+
// Strategy 1: Attribute-reflected properties
|
|
46
|
+
const attrName = this.#toAttributeName(propName);
|
|
47
|
+
if (this.#isAttributeReflected(element, propName, attrName)) {
|
|
48
|
+
return this.#observeAttribute(element, propName, attrName);
|
|
49
|
+
}
|
|
50
|
+
// Strategy 2: Native event (user-driven) + setter interception (programmatic)
|
|
51
|
+
const eventType = this.#getNativeEventType(element, propName);
|
|
52
|
+
if (eventType) {
|
|
53
|
+
return this.#observeHybrid(element, propName, eventType);
|
|
54
|
+
}
|
|
55
|
+
// Strategy 3: Setter interception (custom elements or known prototypes)
|
|
56
|
+
const descriptor = this.#getPropertyDescriptor(element, propName);
|
|
57
|
+
if (descriptor?.set) {
|
|
58
|
+
return this.#interceptSetter(element, propName, descriptor);
|
|
59
|
+
}
|
|
60
|
+
// Strategy 4: Polling fallback
|
|
61
|
+
return this.#observePolling(element, propName);
|
|
62
|
+
}
|
|
63
|
+
// ─── Strategy Implementations ────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Strategy 1: Use MutationObserver on the corresponding attribute.
|
|
66
|
+
*/
|
|
67
|
+
#observeAttribute(element, propName, attrName) {
|
|
68
|
+
const observer = new MutationObserver(() => {
|
|
69
|
+
this.dispatchEvent(new Event(propName));
|
|
70
|
+
});
|
|
71
|
+
observer.observe(element, {
|
|
72
|
+
attributes: true,
|
|
73
|
+
attributeFilter: [attrName],
|
|
74
|
+
});
|
|
75
|
+
return () => observer.disconnect();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Strategy 2: Listen for native events AND intercept setter for programmatic changes.
|
|
79
|
+
*/
|
|
80
|
+
#observeHybrid(element, propName, eventType) {
|
|
81
|
+
const ac = new AbortController();
|
|
82
|
+
const { signal } = ac;
|
|
83
|
+
// Listen for user-driven events
|
|
84
|
+
element.addEventListener(eventType, () => {
|
|
85
|
+
this.dispatchEvent(new Event(propName));
|
|
86
|
+
}, { signal });
|
|
87
|
+
// Also intercept setter for programmatic changes
|
|
88
|
+
const descriptor = this.#getPropertyDescriptor(element, propName);
|
|
89
|
+
let restoreSetter;
|
|
90
|
+
if (descriptor?.set) {
|
|
91
|
+
restoreSetter = this.#interceptSetter(element, propName, descriptor);
|
|
92
|
+
}
|
|
93
|
+
return () => {
|
|
94
|
+
ac.abort();
|
|
95
|
+
restoreSetter?.();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Strategy 3: Override the property setter on the instance to detect programmatic writes.
|
|
100
|
+
*/
|
|
101
|
+
#interceptSetter(element, propName, descriptor) {
|
|
102
|
+
const originalSet = descriptor.set;
|
|
103
|
+
const originalGet = descriptor.get;
|
|
104
|
+
const self = this;
|
|
105
|
+
Object.defineProperty(element, propName, {
|
|
106
|
+
get: originalGet ? function () { return originalGet.call(this); } : undefined,
|
|
107
|
+
set(newValue) {
|
|
108
|
+
const oldValue = originalGet ? originalGet.call(this) : undefined;
|
|
109
|
+
originalSet.call(this, newValue);
|
|
110
|
+
if (newValue !== oldValue) {
|
|
111
|
+
self.dispatchEvent(new Event(propName));
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
configurable: true,
|
|
115
|
+
enumerable: descriptor.enumerable,
|
|
116
|
+
});
|
|
117
|
+
return () => {
|
|
118
|
+
// Restore by deleting instance property — prototype descriptor takes over again
|
|
119
|
+
delete element[propName];
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Strategy 4: Polling via requestAnimationFrame.
|
|
124
|
+
*/
|
|
125
|
+
#observePolling(element, propName) {
|
|
126
|
+
let active = true;
|
|
127
|
+
let lastValue = element[propName];
|
|
128
|
+
const poll = () => {
|
|
129
|
+
if (!active)
|
|
130
|
+
return;
|
|
131
|
+
const currentValue = element[propName];
|
|
132
|
+
if (currentValue !== lastValue) {
|
|
133
|
+
lastValue = currentValue;
|
|
134
|
+
this.dispatchEvent(new Event(propName));
|
|
135
|
+
}
|
|
136
|
+
requestAnimationFrame(poll);
|
|
137
|
+
};
|
|
138
|
+
requestAnimationFrame(poll);
|
|
139
|
+
return () => { active = false; };
|
|
140
|
+
}
|
|
141
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
142
|
+
/**
|
|
143
|
+
* Convert a camelCase property name to its kebab-case attribute equivalent.
|
|
144
|
+
* e.g. ariaValueNow -> aria-valuenow
|
|
145
|
+
*/
|
|
146
|
+
#toAttributeName(propName) {
|
|
147
|
+
// aria properties have a direct mapping
|
|
148
|
+
if (propName.startsWith('aria')) {
|
|
149
|
+
return propName.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
// General camelCase to kebab-case
|
|
152
|
+
return propName.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check if setting a property updates the corresponding attribute.
|
|
156
|
+
*/
|
|
157
|
+
#isAttributeReflected(element, propName, attrName) {
|
|
158
|
+
// For form elements, 'value' attribute is only the default — property changes
|
|
159
|
+
// from user input do NOT update the attribute, so it's not truly reflected.
|
|
160
|
+
const { localName } = element;
|
|
161
|
+
if (propName === 'value' && ['input', 'textarea', 'select', 'button'].includes(localName)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
// For <data>, <meter>, <output>, <progress> the value property reflects to the value attribute
|
|
165
|
+
if (propName === 'value' && ['data', 'meter', 'output', 'progress'].includes(localName)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
// Quick check: does the attribute currently exist or is the property known to reflect?
|
|
169
|
+
if (element.hasAttribute(attrName))
|
|
170
|
+
return true;
|
|
171
|
+
// For aria-* properties, they are spec-defined as reflecting
|
|
172
|
+
if (propName.startsWith('aria'))
|
|
173
|
+
return true;
|
|
174
|
+
// For href, src on appropriate elements
|
|
175
|
+
const reflectedProps = {
|
|
176
|
+
'href': ['a', 'area', 'link', 'base'],
|
|
177
|
+
'src': ['img', 'script', 'iframe', 'audio', 'video', 'source', 'embed'],
|
|
178
|
+
'action': ['form'],
|
|
179
|
+
'value': ['option', 'param', 'li'],
|
|
180
|
+
};
|
|
181
|
+
if (reflectedProps[propName]?.includes(localName))
|
|
182
|
+
return true;
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Determine the native event type that fires when a property changes (user-driven).
|
|
187
|
+
* Leverages Infer's inferEventType for elements known to have input-like behavior.
|
|
188
|
+
*/
|
|
189
|
+
#getNativeEventType(element, propName) {
|
|
190
|
+
const { localName } = element;
|
|
191
|
+
// Only applicable for form-like elements with value/checked properties
|
|
192
|
+
if (['input', 'textarea', 'select'].includes(localName)) {
|
|
193
|
+
if (propName === 'value' || propName === 'valueAsNumber' || propName === 'valueAsDate' || propName === 'checked' || propName === 'selectedIndex') {
|
|
194
|
+
return inferEventType(element);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (localName === 'details' && propName === 'open') {
|
|
198
|
+
return 'toggle';
|
|
199
|
+
}
|
|
200
|
+
// contentEditable elements fire 'input' events on user edits
|
|
201
|
+
if (element instanceof HTMLElement && element.isContentEditable) {
|
|
202
|
+
if (propName === 'textContent' || propName === 'innerHTML' || propName === 'innerText') {
|
|
203
|
+
return 'input';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Walk the prototype chain to find a property descriptor.
|
|
210
|
+
*/
|
|
211
|
+
#getPropertyDescriptor(element, propName) {
|
|
212
|
+
let proto = Object.getPrototypeOf(element);
|
|
213
|
+
while (proto && proto !== Object.prototype) {
|
|
214
|
+
const desc = Object.getOwnPropertyDescriptor(proto, propName);
|
|
215
|
+
if (desc)
|
|
216
|
+
return desc;
|
|
217
|
+
proto = Object.getPrototypeOf(proto);
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Tear down all watchers and clean up resources.
|
|
223
|
+
*/
|
|
224
|
+
destroy() {
|
|
225
|
+
for (const cleanup of this.#watchedProperties.values()) {
|
|
226
|
+
cleanup();
|
|
227
|
+
}
|
|
228
|
+
this.#watchedProperties.clear();
|
|
229
|
+
}
|
|
230
|
+
}
|