@ideasonpurpose/build-tools-wordpress 2.1.4 → 2.1.6

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/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### v2.1.5
8
+
9
+ > 17 April 2025
10
+
11
+ - escape capture group replacement strings when unTokenizing, plus tests
12
+ - change dev site reporter emoji
13
+
14
+ #### v2.1.4
15
+
16
+ > 16 April 2025
17
+
18
+ - use look-behind and look-forward to allow overlaps, simplify replacements
19
+ - fix README
20
+
7
21
  #### v2.1.3
8
22
 
9
23
  > 16 April 2025
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ideasonpurpose/build-tools-wordpress
2
2
 
3
- #### Version 2.1.4
3
+ #### Version 2.1.6
4
4
 
5
5
  [![NPM Version](https://img.shields.io/npm/v/%40ideasonpurpose%2Fbuild-tools-wordpress?logo=npm)](https://www.npmjs.com/package/@ideasonpurpose/build-tools-wordpress)
6
6
  [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ideasonpurpose/build-tools-wordpress/npm-publish.yml?logo=github&logoColor=white)](https://github.com/ideasonpurpose/build-tools-wordpress#readme)
@@ -23,16 +23,26 @@ const phpOptions = prettierConfig.overrides.find(
23
23
  (o) => o.files === "*.php",
24
24
  )?.options;
25
25
 
26
+ const isInTag = (html, offset) => {
27
+ if (offset === 0) return false;
28
+
29
+ for (let i = offset - 1; i >= 0; i--) {
30
+ if (html[i] === "<") {
31
+ return true;
32
+ } else if (html[i] === ">") {
33
+ return false;
34
+ }
35
+ }
36
+ return false;
37
+ };
38
+
26
39
  /**
27
- * Replaces PHP Code Blocks with tokens, returns tokenized HTML and an object containing
28
- * PHP Code Blocks
40
+ * Replaces all PHP Code Blocks with iterated tokens.
29
41
  *
30
- * For code blocks between tags (bounded by > & <) tokens will be self-closing HTML
31
- * tags, similar to this: <php_14______ /> PHP Code blocks at the beginning and end of
32
- * the file will be tokenized as self-closing if they preceed or follow an HTML tag.
42
+ * Code Blocks inside HTML tags will be replaced with attribute-safe tokens: _php_4____
43
+ * All other Code Blocks will be replaced with tag-shaped tokens: <php_4___ />
33
44
  *
34
- * All other PHP Code Blocks are represented as HTML attribute-safe padded strings, up
35
- * to 80 characters long.
45
+ * Tokens will match the length of their Code Blocks up to 80 characters.
36
46
  *
37
47
  * NOTE: Because Prettier's HTML formatter will always add a space before self-closing
38
48
  * tags' closing slash, we just include the space in the token to prevent it from
@@ -40,9 +50,28 @@ const phpOptions = prettierConfig.overrides.find(
40
50
  * to unTokenizeHTML().
41
51
  */
42
52
  export function tokenizeHTML(htmlContent) {
53
+ let tokenizedHTML = "";
43
54
  const phpCodeBlocks = {};
44
55
  let tokenCount = 0;
45
56
 
57
+ /**
58
+ * Check previous content for a '>' or '<' then return either an attribute-safe
59
+ * token: _php_4____ or a tag-shaped token: <php_4___ />
60
+ *
61
+ * NOTE: This uses tokenCount from the enclosing scope
62
+ */
63
+ const tokenizeCodeBlock = (phpCodeBlock, prevContent) => {
64
+ let start = "<";
65
+ let end = " />";
66
+ if (isInTag(prevContent, prevContent.length)) {
67
+ start = "_";
68
+ end = "___";
69
+ }
70
+
71
+ const codeLength = Math.min(phpCodeBlock.length, 80) - end.length;
72
+ return `${start}php_${tokenCount++}__`.padEnd(codeLength, "_") + end;
73
+ };
74
+
46
75
  // const pattern = /<\?(?:php|=)[\s\S]*?\?>/gs;
47
76
  // const pattern =
48
77
  // /(?<before>(?:[^\s]|\s|^)\s*)(?<php><\?(?:php|=).*?(?:\?>|$))(?<after>(?:\s*)[^\s]|$)/gs;
@@ -51,52 +80,39 @@ export function tokenizeHTML(htmlContent) {
51
80
  // // const pattern = /([^\s]+)\s*(<\?(?:php|=).*?(?:\?>|$))\s*([^\s]*)/gms;
52
81
  // const pattern =
53
82
  // /([^\s]?\s*)?(<\?(?:php|=).*?(?:\?>|$))((?:\s*)[^\s]|$)/gms;
54
- const pattern =
55
- /(?<=((?:[^\s]|\s|^)\s*))(<\?(?:php|=).*?\?>)(?=((?:\s*)[^\s]|$))/gms;
56
-
57
- let tokenizedHTML = htmlContent.replace(
58
- pattern,
59
- (string, before, phpCodeBlock, after, offset) => {
60
- const start = [">", ""].includes(before.trim()) ? "<" : "_";
61
- const end = ["<", ""].includes(after.trim()) ? " />" : "___";
62
-
63
- // end-pad the token to the length of the span, up to 80 characters
64
- const codeLength = Math.min(phpCodeBlock.length, 80 - end.length);
65
- const token =
66
- `${start}php_${tokenCount++}__`.padEnd(codeLength, "_") + end;
67
- phpCodeBlocks[token] = phpCodeBlock;
68
-
69
- return token;
70
- },
71
- );
83
+ // const pattern =
84
+ // /(?<=((?:[^\s]|\s|^)\s*))(<\?(?:php|=).*?\?>)(?=((?:\s*)[^\s]|$))/gms;
85
+ // try removing look ahead/behind
86
+ const pattern = /(<\?(?:php|=).*?\?>)/gms;
72
87
 
73
- /**
74
- * special case followup for open-ended PHP tags at the end of the document
75
- * TODO: Merge this back up into a single pattern
76
- */
77
- tokenizedHTML = tokenizedHTML.replace(
78
- /(?<=((?:[^\s]|\s|^)\s*))(<\?(?:php|=).*$)/gms,
88
+ // const regex = new RegExp(/<\?(?:php|=).*?\?>/, "gs");
89
+ // Trying to capture open-ended PHP codeBlocks in a single regexp
90
+ const regex = new RegExp(/<\?(?:php|=).*?(?:\?>|$)/, "gs");
79
91
 
80
- (string, before, phpCodeBlock, offset) => {
81
- const start = [">", ""].includes(before.trim()) ? "<" : "_";
82
- const end = start === "<" ? " />" : "___";
92
+ let match;
93
+ let token;
94
+ let lastIndex = 0;
95
+ while ((match = regex.exec(htmlContent)) !== null) {
96
+ tokenizedHTML += htmlContent.slice(lastIndex, match.index);
83
97
 
84
- const codeLength = Math.min(phpCodeBlock.length, 80 - end.length);
85
- const token =
86
- `${start}php_${tokenCount++}__`.padEnd(codeLength, "_") + end;
87
- phpCodeBlocks[token] = phpCodeBlock;
98
+ token = tokenizeCodeBlock(match[0], tokenizedHTML);
99
+ phpCodeBlocks[token] = match[0];
100
+ tokenizedHTML += token;
88
101
 
89
- return token;
90
- },
91
- );
102
+ lastIndex = match.index + match[0].length;
103
+ }
104
+ tokenizedHTML += htmlContent.slice(lastIndex);
92
105
 
93
106
  return { tokenizedHTML, phpCodeBlocks };
94
107
  }
95
108
 
96
- export function unTokenizeHTML(htmlContent, tokens) {
97
- let phpContent = htmlContent;
98
- for (const token in tokens) {
99
- phpContent = phpContent.replace(new RegExp(token, "g"), tokens[token]);
109
+ export function unTokenizeHTML(tokenizedHTML, phpCodeBlocks) {
110
+ let phpContent = tokenizedHTML;
111
+ for (const token in phpCodeBlocks) {
112
+ phpContent = phpContent.replace(
113
+ new RegExp(token, "g"),
114
+ phpCodeBlocks[token].replace(/\$/g, "$$$$"),
115
+ );
100
116
  }
101
117
  return phpContent;
102
118
  }
@@ -27,7 +27,7 @@ export class AfterDoneReporterPlugin {
27
27
  * NOTE: As of webpack-dev-server vXX.XX.XXX, auto-assigned ports are
28
28
  * not propagated into the compiler.options.devServer object.
29
29
  *
30
- * As a workaround, this relies on the follining line being added to
30
+ * As a workaround, this relies on the following line being added to
31
31
  * the webpack.config file's devServer.setupMiddlewares method:
32
32
  *
33
33
  * devServer.compiler.options.devServer.port = devServer.options.port;
@@ -54,7 +54,7 @@ export class AfterDoneReporterPlugin {
54
54
 
55
55
  setTimeout(() =>
56
56
  // console.log("\n" + this.config.prefix + " " + messages),
57
- console.log( "⚙️ " + messages),
57
+ console.log( "🚧 " + messages),
58
58
  );
59
59
 
60
60
  setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ideasonpurpose/build-tools-wordpress",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "Build scripts and dependencies for IOP's WordPress development environments.",
5
5
  "homepage": "https://github.com/ideasonpurpose/build-tools-wordpress#readme",
6
6
  "bugs": {
@@ -0,0 +1,3 @@
1
+ <i aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa="<?= a('a') ?>"><?= b('b') ?> : <?= c('c') ?></i>
2
+
3
+ <div r="<?= a('a') ?>"><?= b('b') ?> : <?= c('c') ?></div>
@@ -0,0 +1,4 @@
1
+ <?php
2
+ $a = 'a $';
3
+ $b = 'b $&';
4
+ $c = '$0 $1 $2 $3';
@@ -4,7 +4,7 @@ import { describe, expect, test } from "vitest";
4
4
 
5
5
  import { readFile } from "node:fs/promises";
6
6
 
7
- import { tokenizeHTML } from "../bin/format-php-prettier.js";
7
+ import { tokenizeHTML, unTokenizeHTML } from "../bin/format-php-prettier.js";
8
8
 
9
9
  describe("HTML-PHP Prettier", () => {
10
10
  test("All tokens exist", async () => {
@@ -48,7 +48,6 @@ describe("HTML-PHP Prettier", () => {
48
48
  expect(tokens).toHaveLength(1);
49
49
  });
50
50
 
51
-
52
51
  test("bare attribute in tag", async () => {
53
52
  const input = (
54
53
  await readFile(
@@ -56,9 +55,11 @@ describe("HTML-PHP Prettier", () => {
56
55
  )
57
56
  ).toString();
58
57
 
59
- const { phpCodeBlocks: codeBlocks } = tokenizeHTML(input);
58
+ const { tokenizedHTML, phpCodeBlocks } = tokenizeHTML(input);
60
59
 
61
- const tokens = Object.keys(codeBlocks);
60
+ // console.log({input, tokenizedHTML})
61
+
62
+ const tokens = Object.keys(phpCodeBlocks);
62
63
 
63
64
  expect(tokens[0]).toMatch(/^_php_\d+_*$/);
64
65
  expect(tokens[1]).toMatch(/^_php_\d+_*$/);
@@ -68,4 +69,70 @@ describe("HTML-PHP Prettier", () => {
68
69
 
69
70
  expect(tokens).toHaveLength(5);
70
71
  });
72
+
73
+ /**
74
+ * If code blocks contain JS capture-group replacement strings
75
+ * those strings will vanish from the output.
76
+ */
77
+ test("$ capture group references bug", async () => {
78
+ const input = (
79
+ await readFile("./test/fixtures/format-php-prettier/regex-string-bug.php")
80
+ ).toString();
81
+
82
+ const { tokenizedHTML, phpCodeBlocks } = tokenizeHTML(input);
83
+
84
+ const formattedContent = unTokenizeHTML(tokenizedHTML, phpCodeBlocks);
85
+
86
+ expect(formattedContent).toContain("'a $'");
87
+ expect(formattedContent).toContain("'b $&'");
88
+ expect(formattedContent).toContain("'$0 $1 $2 $3'");
89
+ });
90
+
91
+ /**
92
+ * Not sure what's going on here, but this breaks and returns with the last replacement token still in place.
93
+ * Something to do with the overall length, if the aaa...aaa attribute is shortened it formats correctly.
94
+ *
95
+ * Something about this file, likely the length, is causing it to fail to find
96
+ * and restore the last PHP token. Trying to format this:
97
+ *
98
+ *
99
+ * <i aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa="<?= a('a') ?>"><?= b('b') ?> : <?= c('c') ?></i>
100
+ *
101
+ * ...ends up returning something like this:
102
+ *
103
+ * <i aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa="<?= a('a') ?>"
104
+ * ><?= b('b') ?> : _php_3_______
105
+ * /></i>
106
+ *
107
+ * It seems to have something to do with HTML whitespace preservation, I can't get this to
108
+ * happen with a div
109
+ */
110
+ test("whitespace orphaned tokens", async () => {
111
+ const input = (
112
+ await readFile(
113
+ "./test/fixtures/format-php-prettier/length-orphaned-token-bug.php",
114
+ )
115
+ ).toString();
116
+
117
+ const { tokenizedHTML, phpCodeBlocks } = tokenizeHTML(input);
118
+
119
+ const formattedContent = unTokenizeHTML(tokenizedHTML, phpCodeBlocks);
120
+
121
+ // console.log(input, tokenizedHTML, phpCodeBlocks, formattedContent);
122
+
123
+ expect(formattedContent).toContain("<?= c('c') ?>");
124
+ });
125
+
126
+ test("tokenization error (drill down from whitespace orphaned tokens)", async () => {
127
+ const input = "<?= b('b') ?> : <?= c('c') ?>";
128
+ const { tokenizedHTML, phpCodeBlocks } = tokenizeHTML(input);
129
+
130
+ // console.log({input, tokenizedHTML, phpCodeBlocks});
131
+
132
+
133
+ expect(tokenizedHTML).toContain("<php_0____ />");
134
+ expect(tokenizedHTML).toContain("<php_1____ />");
135
+ expect(phpCodeBlocks).toHaveProperty("<php_0____ />");
136
+ expect(phpCodeBlocks).toHaveProperty("<php_1____ />");
137
+ });
71
138
  });