@gxp-dev/tools 2.0.31 → 2.0.32
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/package.json +1 -1
- package/runtime/vite-source-tracker-plugin.js +319 -202
package/package.json
CHANGED
|
@@ -18,215 +18,332 @@
|
|
|
18
18
|
* <div v-html="content" data-gxp-expr="v-html:content"></div>
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Parse HTML tags using a state machine approach
|
|
23
|
+
* This correctly handles > characters inside quoted attribute values
|
|
24
|
+
*/
|
|
25
|
+
function parseTagsFromHtml(html) {
|
|
26
|
+
const tags = [];
|
|
27
|
+
let i = 0;
|
|
28
|
+
|
|
29
|
+
const VOID_ELEMENTS = new Set([
|
|
30
|
+
"br", "hr", "img", "input", "meta", "link",
|
|
31
|
+
"area", "base", "col", "embed", "param",
|
|
32
|
+
"source", "track", "wbr"
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
while (i < html.length) {
|
|
36
|
+
// Look for tag start
|
|
37
|
+
if (html[i] === "<") {
|
|
38
|
+
const tagStart = i;
|
|
39
|
+
i++;
|
|
40
|
+
|
|
41
|
+
// Check for closing tag
|
|
42
|
+
const isClosing = html[i] === "/";
|
|
43
|
+
if (isClosing) i++;
|
|
44
|
+
|
|
45
|
+
// Skip if not a valid tag start (could be < in text or comment)
|
|
46
|
+
if (!/[a-zA-Z]/.test(html[i])) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Parse tag name
|
|
51
|
+
let tagName = "";
|
|
52
|
+
while (i < html.length && /[a-zA-Z0-9-]/.test(html[i])) {
|
|
53
|
+
tagName += html[i];
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse attributes (skip whitespace and attributes until we hit > or />)
|
|
58
|
+
let attrs = "";
|
|
59
|
+
let inDoubleQuote = false;
|
|
60
|
+
let inSingleQuote = false;
|
|
61
|
+
let foundEnd = false;
|
|
62
|
+
let isSelfClosing = false;
|
|
63
|
+
|
|
64
|
+
while (i < html.length && !foundEnd) {
|
|
65
|
+
const char = html[i];
|
|
66
|
+
|
|
67
|
+
if (inDoubleQuote) {
|
|
68
|
+
attrs += char;
|
|
69
|
+
if (char === '"') {
|
|
70
|
+
inDoubleQuote = false;
|
|
71
|
+
}
|
|
72
|
+
i++;
|
|
73
|
+
} else if (inSingleQuote) {
|
|
74
|
+
attrs += char;
|
|
75
|
+
if (char === "'") {
|
|
76
|
+
inSingleQuote = false;
|
|
77
|
+
}
|
|
78
|
+
i++;
|
|
79
|
+
} else if (char === '"') {
|
|
80
|
+
attrs += char;
|
|
81
|
+
inDoubleQuote = true;
|
|
82
|
+
i++;
|
|
83
|
+
} else if (char === "'") {
|
|
84
|
+
attrs += char;
|
|
85
|
+
inSingleQuote = true;
|
|
86
|
+
i++;
|
|
87
|
+
} else if (char === "/" && html[i + 1] === ">") {
|
|
88
|
+
isSelfClosing = true;
|
|
89
|
+
i += 2;
|
|
90
|
+
foundEnd = true;
|
|
91
|
+
} else if (char === ">") {
|
|
92
|
+
i++;
|
|
93
|
+
foundEnd = true;
|
|
94
|
+
} else {
|
|
95
|
+
attrs += char;
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!foundEnd) {
|
|
101
|
+
// Malformed tag, skip
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fullMatch = html.substring(tagStart, i);
|
|
106
|
+
|
|
107
|
+
// Check for void elements
|
|
108
|
+
if (!isSelfClosing && VOID_ELEMENTS.has(tagName.toLowerCase())) {
|
|
109
|
+
isSelfClosing = true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
tags.push({
|
|
113
|
+
tagName,
|
|
114
|
+
attrs: attrs.trim(),
|
|
115
|
+
start: tagStart,
|
|
116
|
+
fullMatch,
|
|
117
|
+
isClosing,
|
|
118
|
+
isSelfClosing,
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return tags;
|
|
126
|
+
}
|
|
127
|
+
|
|
21
128
|
/**
|
|
22
129
|
* Create the source tracker plugin
|
|
23
130
|
*/
|
|
24
131
|
export function gxpSourceTrackerPlugin(options = {}) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
132
|
+
const { enabled = true, attrName = "data-gxp-expr" } = options;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
name: "gxp-source-tracker",
|
|
136
|
+
enforce: "pre",
|
|
137
|
+
|
|
138
|
+
apply(config, { command }) {
|
|
139
|
+
return enabled && command === "serve";
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
transform(code, id) {
|
|
143
|
+
if (!id.endsWith(".vue")) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const templateMatch = code.match(/<template>([\s\S]*?)<\/template>/);
|
|
148
|
+
if (!templateMatch) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Debug: log which file we're processing
|
|
153
|
+
const fileName = id.split("/").pop();
|
|
154
|
+
console.log(`[GxP Source Tracker] Processing: ${fileName}`);
|
|
155
|
+
|
|
156
|
+
let template = templateMatch[1];
|
|
157
|
+
let modified = false;
|
|
158
|
+
|
|
159
|
+
// Process each element that has {{ expression }} as its content
|
|
160
|
+
// Strategy: Find each {{ expression }} and trace back to find its parent element
|
|
161
|
+
|
|
162
|
+
const exprPattern = /\{\{([\s\S]*?)\}\}/g;
|
|
163
|
+
let match;
|
|
164
|
+
|
|
165
|
+
// Map to track which elements we've already added attributes to
|
|
166
|
+
// Key: element start position, Value: array of expressions
|
|
167
|
+
const elementExpressions = new Map();
|
|
168
|
+
|
|
169
|
+
while ((match = exprPattern.exec(template)) !== null) {
|
|
170
|
+
const exprStart = match.index;
|
|
171
|
+
const expression = match[1].trim();
|
|
172
|
+
|
|
173
|
+
// Find the opening tag that contains this expression
|
|
174
|
+
// Look backwards from the expression to find the nearest unclosed tag
|
|
175
|
+
const beforeExpr = template.substring(0, exprStart);
|
|
176
|
+
|
|
177
|
+
// Use state-machine parser for reliable tag parsing
|
|
178
|
+
const tags = parseTagsFromHtml(beforeExpr);
|
|
179
|
+
|
|
180
|
+
// Walk through tags to find the immediate parent (last unclosed tag)
|
|
181
|
+
const stack = [];
|
|
182
|
+
for (const tag of tags) {
|
|
183
|
+
if (tag.isSelfClosing) continue;
|
|
184
|
+
|
|
185
|
+
if (tag.isClosing) {
|
|
186
|
+
// Pop from stack
|
|
187
|
+
if (
|
|
188
|
+
stack.length > 0 &&
|
|
189
|
+
stack[stack.length - 1].tagName.toLowerCase() ===
|
|
190
|
+
tag.tagName.toLowerCase()
|
|
191
|
+
) {
|
|
192
|
+
stack.pop();
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
stack.push(tag);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The last item in the stack is our parent element
|
|
200
|
+
if (stack.length > 0) {
|
|
201
|
+
const parentTagInfo = stack[stack.length - 1];
|
|
202
|
+
|
|
203
|
+
// Skip script/style tags
|
|
204
|
+
if (
|
|
205
|
+
["script", "style"].includes(parentTagInfo.tagName.toLowerCase())
|
|
206
|
+
) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Skip if already has data-gxp-expr in the original attrs
|
|
211
|
+
if (parentTagInfo.attrs.includes(attrName)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Add this expression to the parent element's list
|
|
216
|
+
const key = parentTagInfo.start;
|
|
217
|
+
if (!elementExpressions.has(key)) {
|
|
218
|
+
elementExpressions.set(key, {
|
|
219
|
+
tagInfo: parentTagInfo,
|
|
220
|
+
expressions: [],
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
elementExpressions.get(key).expressions.push(expression);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Now build replacements for each element that has expressions
|
|
228
|
+
const replacements = [];
|
|
229
|
+
|
|
230
|
+
for (const [start, data] of elementExpressions) {
|
|
231
|
+
const { tagInfo, expressions } = data;
|
|
232
|
+
|
|
233
|
+
// Only add expressions that are direct children (not from nested elements)
|
|
234
|
+
// For now, take all unique expressions
|
|
235
|
+
const uniqueExprs = [...new Set(expressions)];
|
|
236
|
+
const exprValue = uniqueExprs.join("; ");
|
|
237
|
+
|
|
238
|
+
const oldOpenTag = tagInfo.fullMatch;
|
|
239
|
+
// Insert the attribute before the closing > or />
|
|
240
|
+
let newOpenTag;
|
|
241
|
+
if (oldOpenTag.endsWith("/>")) {
|
|
242
|
+
newOpenTag = oldOpenTag.slice(0, -2) + ` ${attrName}="${escapeAttr(exprValue)}"/>`;
|
|
243
|
+
} else {
|
|
244
|
+
newOpenTag = oldOpenTag.slice(0, -1) + ` ${attrName}="${escapeAttr(exprValue)}">`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
replacements.push({
|
|
248
|
+
start: tagInfo.start,
|
|
249
|
+
oldText: oldOpenTag,
|
|
250
|
+
newText: newOpenTag,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
modified = true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Apply replacements in reverse order to preserve indices
|
|
257
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
258
|
+
for (const r of replacements) {
|
|
259
|
+
template =
|
|
260
|
+
template.substring(0, r.start) +
|
|
261
|
+
r.newText +
|
|
262
|
+
template.substring(r.start + r.oldText.length);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Also handle v-html, v-text directives using state-machine approach
|
|
266
|
+
template = addAttrToDirective(template, "v-html", attrName);
|
|
267
|
+
template = addAttrToDirective(template, "v-text", attrName);
|
|
268
|
+
template = addAttrToDirective(template, ":textContent", attrName);
|
|
269
|
+
|
|
270
|
+
if (!modified && !template.includes(attrName)) {
|
|
271
|
+
console.log(
|
|
272
|
+
`[GxP Source Tracker] No expressions found in: ${fileName}`
|
|
273
|
+
);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log(
|
|
278
|
+
`[GxP Source Tracker] Added data-gxp-expr to ${elementExpressions.size} elements in: ${fileName}`
|
|
279
|
+
);
|
|
280
|
+
const newCode = code.replace(
|
|
281
|
+
/<template>[\s\S]*?<\/template>/,
|
|
282
|
+
`<template>${template}</template>`
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
code: newCode,
|
|
287
|
+
map: null,
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Add data-gxp-expr attribute to elements with a specific directive
|
|
295
|
+
*/
|
|
296
|
+
function addAttrToDirective(template, directive, attrName) {
|
|
297
|
+
const tags = parseTagsFromHtml(template);
|
|
298
|
+
const replacements = [];
|
|
299
|
+
|
|
300
|
+
for (const tag of tags) {
|
|
301
|
+
if (tag.isClosing) continue;
|
|
302
|
+
if (tag.fullMatch.includes(attrName)) continue;
|
|
303
|
+
|
|
304
|
+
// Check if this tag has the directive
|
|
305
|
+
const directivePattern = new RegExp(`${escapeRegex(directive)}="([^"]*)"`, "g");
|
|
306
|
+
const match = directivePattern.exec(tag.attrs);
|
|
307
|
+
if (match) {
|
|
308
|
+
const exprValue = `${directive}:${match[1]}`;
|
|
309
|
+
let newOpenTag;
|
|
310
|
+
if (tag.fullMatch.endsWith("/>")) {
|
|
311
|
+
newOpenTag = tag.fullMatch.slice(0, -2) + ` ${attrName}="${escapeAttr(exprValue)}"/>`;
|
|
312
|
+
} else {
|
|
313
|
+
newOpenTag = tag.fullMatch.slice(0, -1) + ` ${attrName}="${escapeAttr(exprValue)}">`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
replacements.push({
|
|
317
|
+
start: tag.start,
|
|
318
|
+
oldText: tag.fullMatch,
|
|
319
|
+
newText: newOpenTag,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Apply replacements in reverse order
|
|
325
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
326
|
+
for (const r of replacements) {
|
|
327
|
+
template =
|
|
328
|
+
template.substring(0, r.start) +
|
|
329
|
+
r.newText +
|
|
330
|
+
template.substring(r.start + r.oldText.length);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return template;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function escapeRegex(str) {
|
|
337
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
221
338
|
}
|
|
222
339
|
|
|
223
340
|
function escapeAttr(str) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
341
|
+
return str
|
|
342
|
+
.replace(/&/g, "&")
|
|
343
|
+
.replace(/"/g, """)
|
|
344
|
+
.replace(/'/g, "'")
|
|
345
|
+
.replace(/</g, "<")
|
|
346
|
+
.replace(/>/g, ">");
|
|
230
347
|
}
|
|
231
348
|
|
|
232
349
|
export default gxpSourceTrackerPlugin;
|