@gxp-dev/tools 2.0.31 → 2.0.33

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 CHANGED
@@ -22,8 +22,6 @@ Create a new GxP plugin project:
22
22
  gxdev init my-plugin
23
23
  cd my-plugin
24
24
  git init
25
- git submodule add git@bitbucket.org:gramercytech/z-plugin-components.git src/z-components
26
- git submodule update
27
25
  npm run dev
28
26
  ```
29
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gxp-dev/tools",
3
- "version": "2.0.31",
3
+ "version": "2.0.33",
4
4
  "description": "Dev tools to create platform plugins",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -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
- const {
26
- enabled = true,
27
- attrName = 'data-gxp-expr'
28
- } = options;
29
-
30
- return {
31
- name: 'gxp-source-tracker',
32
- enforce: 'pre',
33
-
34
- apply(config, { command }) {
35
- return enabled && command === 'serve';
36
- },
37
-
38
- transform(code, id) {
39
- if (!id.endsWith('.vue')) {
40
- return null;
41
- }
42
-
43
- const templateMatch = code.match(/<template>([\s\S]*?)<\/template>/);
44
- if (!templateMatch) {
45
- return null;
46
- }
47
-
48
- // Debug: log which file we're processing
49
- const fileName = id.split('/').pop();
50
- console.log(`[GxP Source Tracker] Processing: ${fileName}`);
51
-
52
- let template = templateMatch[1];
53
- let modified = false;
54
-
55
- // Track which elements we've already processed to avoid duplicates
56
- const processed = new Set();
57
-
58
- // Process each element that has {{ expression }} as its content
59
- // We need to find the element that DIRECTLY contains the expression
60
- // Strategy: Find all {{ expr }} and trace back to their parent element
61
-
62
- // Strategy: Find each {{ expression }} and trace back to find its parent element
63
- // This handles cases where expressions are nested or mixed with other content
64
-
65
- const exprPattern = /\{\{([\s\S]*?)\}\}/g;
66
- let match;
67
-
68
- // Map to track which elements we've already added attributes to
69
- // Key: element start position, Value: array of expressions
70
- const elementExpressions = new Map();
71
-
72
- while ((match = exprPattern.exec(template)) !== null) {
73
- const exprStart = match.index;
74
- const expression = match[1].trim();
75
-
76
- // Find the opening tag that contains this expression
77
- // Look backwards from the expression to find the nearest unclosed tag
78
- const beforeExpr = template.substring(0, exprStart);
79
-
80
- // Find the last opening tag before this expression
81
- // We need to find a tag that hasn't been closed yet
82
- let depth = 0;
83
- let tagMatch;
84
- let parentTagInfo = null;
85
-
86
- // Find all tags before this expression
87
- const tagPattern = /<\/?([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>/g;
88
- const tags = [];
89
-
90
- while ((tagMatch = tagPattern.exec(beforeExpr)) !== null) {
91
- const isClosing = tagMatch[0].startsWith('</');
92
- const isSelfClosing = tagMatch[0].endsWith('/>') ||
93
- ['br', 'hr', 'img', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'param', 'source', 'track', 'wbr'].includes(tagMatch[1].toLowerCase());
94
-
95
- tags.push({
96
- tagName: tagMatch[1],
97
- attrs: tagMatch[2],
98
- start: tagMatch.index,
99
- fullMatch: tagMatch[0],
100
- isClosing,
101
- isSelfClosing
102
- });
103
- }
104
-
105
- // Walk through tags to find the immediate parent (last unclosed tag)
106
- const stack = [];
107
- for (const tag of tags) {
108
- if (tag.isSelfClosing) continue;
109
-
110
- if (tag.isClosing) {
111
- // Pop from stack
112
- if (stack.length > 0 && stack[stack.length - 1].tagName.toLowerCase() === tag.tagName.toLowerCase()) {
113
- stack.pop();
114
- }
115
- } else {
116
- stack.push(tag);
117
- }
118
- }
119
-
120
- // The last item in the stack is our parent element
121
- if (stack.length > 0) {
122
- parentTagInfo = stack[stack.length - 1];
123
-
124
- // Skip script/style tags
125
- if (['script', 'style'].includes(parentTagInfo.tagName.toLowerCase())) {
126
- continue;
127
- }
128
-
129
- // Skip if already has data-gxp-expr in the original attrs
130
- if (parentTagInfo.attrs.includes(attrName)) {
131
- continue;
132
- }
133
-
134
- // Add this expression to the parent element's list
135
- const key = parentTagInfo.start;
136
- if (!elementExpressions.has(key)) {
137
- elementExpressions.set(key, {
138
- tagInfo: parentTagInfo,
139
- expressions: []
140
- });
141
- }
142
- elementExpressions.get(key).expressions.push(expression);
143
- }
144
- }
145
-
146
- // Now build replacements for each element that has expressions
147
- const replacements = [];
148
-
149
- for (const [start, data] of elementExpressions) {
150
- const { tagInfo, expressions } = data;
151
-
152
- // Only add expressions that are direct children (not from nested elements)
153
- // For now, take all unique expressions
154
- const uniqueExprs = [...new Set(expressions)];
155
- const exprValue = uniqueExprs.join('; ');
156
-
157
- const oldOpenTag = tagInfo.fullMatch;
158
- // Insert the attribute before the closing >
159
- const newOpenTag = oldOpenTag.replace(/>$/, ` ${attrName}="${escapeAttr(exprValue)}">`);
160
-
161
- replacements.push({
162
- start: tagInfo.start,
163
- oldText: oldOpenTag,
164
- newText: newOpenTag
165
- });
166
-
167
- modified = true;
168
- }
169
-
170
- // Apply replacements in reverse order to preserve indices
171
- replacements.sort((a, b) => b.start - a.start);
172
- for (const r of replacements) {
173
- template = template.substring(0, r.start) +
174
- r.newText +
175
- template.substring(r.start + r.oldText.length);
176
- }
177
-
178
- // Also handle v-html, v-text directives
179
- // Add data-gxp-expr to elements that have these
180
- template = template.replace(
181
- /<([a-zA-Z][a-zA-Z0-9-]*)([^>]*)(v-html)="([^"]+)"([^>]*)>/g,
182
- (match, tag, before, directive, expr, after) => {
183
- if (match.includes(attrName)) return match;
184
- return `<${tag}${before}${directive}="${expr}"${after} ${attrName}="${escapeAttr('v-html:' + expr)}">`;
185
- }
186
- );
187
-
188
- template = template.replace(
189
- /<([a-zA-Z][a-zA-Z0-9-]*)([^>]*)(v-text)="([^"]+)"([^>]*)>/g,
190
- (match, tag, before, directive, expr, after) => {
191
- if (match.includes(attrName)) return match;
192
- modified = true;
193
- return `<${tag}${before}${directive}="${expr}"${after} ${attrName}="${escapeAttr('v-text:' + expr)}">`;
194
- }
195
- );
196
-
197
- // Handle :textContent binding
198
- template = template.replace(
199
- /<([a-zA-Z][a-zA-Z0-9-]*)([^>]*):textContent="([^"]+)"([^>]*)>/g,
200
- (match, tag, before, expr, after) => {
201
- if (match.includes(attrName)) return match;
202
- modified = true;
203
- return `<${tag}${before}:textContent="${expr}"${after} ${attrName}="${escapeAttr(':textContent:' + expr)}">`;
204
- }
205
- );
206
-
207
- if (!modified) {
208
- console.log(`[GxP Source Tracker] No expressions found in: ${fileName}`);
209
- return null;
210
- }
211
-
212
- console.log(`[GxP Source Tracker] Added data-gxp-expr to ${elementExpressions.size} elements in: ${fileName}`);
213
- const newCode = code.replace(/<template>[\s\S]*?<\/template>/, `<template>${template}</template>`);
214
-
215
- return {
216
- code: newCode,
217
- map: null
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
- return str
225
- .replace(/&/g, '&amp;')
226
- .replace(/"/g, '&quot;')
227
- .replace(/'/g, '&#39;')
228
- .replace(/</g, '&lt;')
229
- .replace(/>/g, '&gt;');
341
+ return str
342
+ .replace(/&/g, "&amp;")
343
+ .replace(/"/g, "&quot;")
344
+ .replace(/'/g, "&#39;")
345
+ .replace(/</g, "&lt;")
346
+ .replace(/>/g, "&gt;");
230
347
  }
231
348
 
232
349
  export default gxpSourceTrackerPlugin;