@ideasonpurpose/build-tools-wordpress 2.10.6 → 2.10.8

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
@@ -1,99 +1,281 @@
1
1
  # @ideasonpurpose/build-tools-wordpress
2
2
 
3
- #### Version 2.10.6
3
+ #### Version 2.10.8
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
- [![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)
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/actions/workflows/npm-publish.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
7
8
 
8
- Build scripts and dependencies for IOP's WordPress development environments.
9
+ Build scripts and shared dependencies for WordPress development. Used in production at [Ideas On Purpose](https://www.ideasonpurpose.com).
9
10
 
10
- ## About This Package
11
+ This package centralizes Webpack configuration, asset pipelines, and helper CLIs so host projects stay thin: a small `package.json`, a one-line `webpack.config.js`, and an optional config file.
11
12
 
12
- These tools were migrated from our [Docker-based WordPress build tools](https://github.com/ideasonpurpose/docker-build) to speed up development and began the process of moving our build tools away from webpack. Gathering dependencies also simplifies the package.json and configuration files in host projects, making those slightly more manageable.
13
+ ## Requirements
13
14
 
14
- ### Versioned Releases
15
+ - **Node.js** 22.15+ (required by webpack-dev-server 6; ESM only)
15
16
 
16
- IOP versions our themes so every release creates a clear rollback snapshot. To accomplish this, every milestone build is generated into a versioned directory. This works well for themes where only one can be active, but fails for plugins where multiple versions can be simultaneously enabled so long as their directory names are different.
17
+ ## Install
17
18
 
18
- To work around this, a `type` property can be added to the config file. When `type` is `plugin`, builds will not add the version to directory names.
19
+ ```sh
20
+ npm install -D @ideasonpurpose/build-tools-wordpress
21
+ ```
19
22
 
20
- ### Optional Config
23
+ Typical host scripts (see [`boilerplate/package.json`](./boilerplate/package.json)):
21
24
 
22
- Each project may optionally include an **ideasonpurpose.config.js** file in the same directory as **package.json**. Any settings in this file will override [the defaults](https://github.com/ideasonpurpose/build-tools-wordpress/blob/main/config/ideasonpurpose.config.js).
25
+ ```json
26
+ {
27
+ "type": "module",
28
+ "scripts": {
29
+ "prebuild": "npm run clean",
30
+ "build": "NODE_ENV=production webpack",
31
+ "postbuild": "npm run zip",
32
+ "start": "webpack serve",
33
+ "zip": "iop-build-zip-archive"
34
+ },
35
+ "devDependencies": {
36
+ "@ideasonpurpose/build-tools-wordpress": "^2.10.6"
37
+ },
38
+ "prettier": "@ideasonpurpose/prettier-config",
39
+ "stylelint": {
40
+ "extends": "@ideasonpurpose/stylelint-config"
41
+ }
42
+ }
43
+ ```
23
44
 
24
- <!--
45
+ Prettier and Stylelint configs are re-exported from this package’s dependencies so hosts can extend them without separate installs.
25
46
 
26
- - **`src`** - The **source** directory containing source files which should be compiled or transposed. The contents of this directory will be omitted from builds.
27
- - **`dist`** - The **distribution** directory where processed, production-ready files will be output to. All contents of this directory will be included in builds.
28
- - **`src`** - An array of file entry points relative to the `src` directory. Each entry point will generate a like-named output file. All files and assets imported by a given entry point will be accessible from that entry's corresponding output file.
47
+ ## Quick start
29
48
 
30
- -->
49
+ **1. Point Webpack at this package**
31
50
 
32
- ## SVG Processing
51
+ ```js
52
+ // webpack.config.js
53
+ export { webpackConfig as default } from "@ideasonpurpose/build-tools-wordpress";
54
+ ```
33
55
 
34
- Webpack handles SVG files differently based on import context:
56
+ **2. Lay out sources** under the default theme paths (package `name` → theme folder):
35
57
 
36
- - **In SCSS files**: SVGs referenced via `url('file.svg')` are inlined as data URIs if under 4KB, otherwise emitted as separate files in the output directory.
58
+ ```text
59
+ wp-content/themes/<package-name>/
60
+ src/
61
+ js/main.js
62
+ js/admin.js
63
+ js/editor.js
64
+ sass/ # or styles/
65
+ dist/ # build output
66
+ ```
37
67
 
38
- - **In JS/TSX files**:
39
- - With `?url` query (e.g., `import svg from 'file.svg?url'`): Treated as assets, inlined as data URIs if under 4KB, else files.
40
- - With `?react` query (e.g., `import Icon from 'file.svg?react'`): Explicitly converted to a React component using `@svgr/webpack`.
41
- - Without `?url` or `?react`: Converted to React components using `@svgr/webpack` for direct JSX usage (default behavior when imported from `.jsx`/`.tsx`).
68
+ **3. Run**
42
69
 
43
- React components imported via `?react` (or implicitly) have `width`/`height` removed (`dimensions: false`), making them responsive while preserving `viewBox`.
70
+ ```sh
71
+ npm start # webpack-dev-server (proxies local WordPress when available)
72
+ NODE_ENV=production npm run build # production build + zip (if postbuild is set)
73
+ ```
44
74
 
45
- Data URIs are generated as `data:image/svg+xml,<url-encoded-content>` using more efficient URL-encoding, not base64.
75
+ Full IOP projects also use Docker Compose tooling from this package (`tooling/docker-compose.yml`) and scripts like `bootstrap`, `db:dump`, and `project:refresh`. Those are optional if you only need the asset pipeline.
46
76
 
47
- ### SVG Optimization
77
+ ## Configuration
48
78
 
49
- SVGO is included along with our preferred [svgo.config.mjs][]. Run this with `npx svgo <file-to-optimize.svg>`
79
+ Optional config is loaded with [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) under the name `ideasonpurpose` typically `ideasonpurpose.config.js` next to `package.json`. Values merge over [defaults](./lib/buildConfig.js).
50
80
 
51
- ## Experimental formatting helpers
81
+ ### Defaults
52
82
 
53
- This package includes two experimental formatting scripts:
83
+ | Option | Default | Notes |
84
+ | -------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------- |
85
+ | `src` | `./wp-content/themes/<name>/src` | `<name>` from host `package.json` |
86
+ | `dist` | `./wp-content/themes/<name>/dist` | |
87
+ | `entry` | `["./js/main.js", "./js/admin.js", "./js/editor.js"]` | Relative to `src`. String, array, or object |
88
+ | `publicPath` | `/wp-content/themes/<name>/dist/` | |
89
+ | `esTarget` | `"es2020"` | Passed to esbuild-loader / minimizer |
90
+ | `devtool` | `"source-map"` | |
91
+ | `proxy` | `"wordpress"` | Dev-server proxy target (Docker service name, URL, IP, or `true` to auto-detect) |
92
+ | `manifestFile` | `"./dependency-manifest.json"` | Written into `dist` |
93
+ | `type` | _(unset / theme)_ | Set `"plugin"` to change archive naming |
54
94
 
55
- ### `iop-html-php-prettier`
95
+ Paths resolve relative to the config file (or `package.json` if no config file is found).
56
96
 
57
- This uses [Prettier](https://prettier.io) to double-format mixed HTML and PHP files (like WordPress templates). PHP blocks are tokenized so Prettier can safely format the file as HTML, then PHP blocks are restored and the file is formatted again as PHP using [@prettier/plugin-php](https://github.com/prettier/plugin-php).
58
- **Usage:** `npx iop-html-php-prettier path/to/file.php`
97
+ ### Entry shapes
59
98
 
60
- ### `iop-format-wp-block-pattern`
99
+ ```js
100
+ // Array → basenames become entry keys (overlapping basenames merge)
101
+ entry: ["./js/main.js", "./js/admin.js"];
102
+
103
+ // String → single entry
104
+ entry: "./js/main.js";
105
+
106
+ // Object → passed through
107
+ entry: { app: "./js/main.js", admin: "./js/admin.js" };
108
+ ```
109
+
110
+ ### Themes vs plugins
111
+
112
+ Releases are versioned so each build is a clear rollback snapshot. For **themes**, zip archives use a versioned folder name (`my-theme-1.2.3`). That fails for **plugins**, where multiple versions can be active if directory names differ.
113
+
114
+ Set `type: "plugin"` to omit the version from the archive directory name:
115
+
116
+ ```js
117
+ // ideasonpurpose.config.js
118
+ export default {
119
+ type: "plugin",
120
+ src: `./wp-content/plugins/my-plugin/src`,
121
+ dist: `./wp-content/plugins/my-plugin/dist`,
122
+ publicPath: `/wp-content/plugins/my-plugin/dist/`,
123
+ entry: ["./js/main.js"],
124
+ };
125
+ ```
126
+
127
+ ## What the build does
128
+
129
+ | Area | Implementation |
130
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
131
+ | Bundler | Webpack 5 |
132
+ | JS/TS/JSX | [esbuild-loader](https://github.com/privatenumber/esbuild-loader) |
133
+ | CSS | Sass ([sass-embedded](https://sass-lang.com/dart-sass/), modern compiler API), PostCSS (autoprefixer; cssnano in production) |
134
+ | Images | `asset` modules + [Sharp](https://sharp.pixelplumbing.com/) via image-minimizer-webpack-plugin |
135
+ | SVG | Asset / data-URI or React components via [@svgr/webpack](https://react-svgr.com/) (see below) |
136
+ | Copy | Static files from `src` (blocks, fonts, etc. handled specially) |
137
+ | WordPress | [`@wordpress/dependency-extraction-webpack-plugin`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dependency-extraction-webpack-plugin/) + custom dependency manifest |
138
+ | Dev server | Hot reload, live reload on PHP/HTML/SVG/JSON, reverse proxy to local WP |
139
+ | Production | Content hashes, minification, optional bundle analyzer report, zip via `iop-build-zip-archive` |
140
+
141
+ Sass `loadPaths` include `src/sass`, `src/styles`, and the project `node_modules`, so packages can be imported by name:
142
+
143
+ ```scss
144
+ @use "some-npm-package/styles";
145
+ ```
146
+
147
+ ### Dev server
148
+
149
+ - Proxies the site to a local WordPress container (default Docker service name `wordpress`) when `proxy` is enabled
150
+ - Watches theme PHP/HTML/SVG/JSON for full reload
151
+ - `GET /webpack/reload` triggers a client refresh (useful from PHP or external tools)
152
+
153
+ ### Zip archives
154
+
155
+ `iop-build-zip-archive` packs the theme/plugin parent of `src` into `_builds/<name>[-version].zip`. Version suffix is skipped when `type` is `"plugin"`.
156
+
157
+ ## SVG processing
158
+
159
+ Webpack treats SVGs differently by context:
160
+
161
+ - **In SCSS** (`url('file.svg')`): inlined as a data URI if under 4KB, otherwise emitted as a file.
162
+ - **In JS/TSX**:
163
+ - `import svg from 'file.svg?url'` — asset URL / data URI (4KB threshold)
164
+ - `import Icon from 'file.svg?react'` — React component via SVGR
165
+ - bare `import Icon from 'file.svg'` from `.jsx`/`.tsx` — React component (default)
166
+
167
+ React SVG components use `dimensions: false` (no fixed `width`/`height`; `viewBox` kept) so they scale with CSS.
61
168
 
62
- A specialized formatter for WordPress PHP block patterns. This makes block markup easier to read, expands JSON settings, and adjusts whitespace to work around Block Editor quirks.
63
- **Usage:** `npx iop-format-wp-block-pattern path/to/pattern.php`
169
+ Data URIs use URL-encoding (`data:image/svg+xml,<encoded>`), not base64.
64
170
 
65
- ## Local Development
171
+ ### SVGO and VS Code
66
172
 
67
- Because this project makes use of bin scripts, conventional `npm link` workflows won't work correctly. To work on this code in a development project, change the project's package.json to install from a local file path, probably something like this:
173
+ [SVGO](https://github.com/svg/svgo) and our preferred [`config/svgo.config.mjs`](./config/svgo.config.mjs) ship with the package, plus a stdin formatter for the editor.
174
+
175
+ Install [SVG Language Mode ID](https://marketplace.visualstudio.com/items?itemName=ideasonpurpose.svg-language-mode-id) and [Custom Local Formatters](https://marketplace.visualstudio.com/items?itemName=jkillian.custom-local-formatters), then in `settings.json`:
68
176
 
69
177
  ```json
70
- "devDependencies": {
71
- "@ideasonpurpose/build-tools-wordpress": "../../build-tools-wordpress"
178
+ {
179
+ "[svg]": {
180
+ "editor.defaultFormatter": "jkillian.custom-local-formatters",
181
+ "editor.formatOnSave": false
182
+ },
183
+ "customLocalFormatters.formatters": [
184
+ {
185
+ "command": "iop-vscode-svgo",
186
+ "languages": ["svg"]
187
+ }
188
+ ]
189
+ }
190
+ ```
191
+
192
+ The formatter loads a project `svgo.config` if present, otherwise falls back to this package’s config.
193
+
194
+ Notes:
195
+
196
+ - VS Code treats SVG as XML by default; the language-mode extension is required for the `[svg]` scope.
197
+ - Formatting fails if `editor.formatOnSaveMode` is `"modifications"` (range formatting does not send a full document).
198
+
199
+ **IOP SVGO defaults** ([`config/svgo.config.mjs`](./config/svgo.config.mjs)):
200
+
201
+ - Pretty-print with 4-space indent
202
+ - Preserve ID names (`cleanupIds: false`)
203
+ - Custom: copy width/height from `viewBox` onto `<svg>` when missing
204
+ - Custom: remove top-level `fill="none"`
205
+ - `removeUselessStrokeAndFill` with `removeNone: true`
206
+
207
+ ## CLI tools
208
+
209
+ | Command | Purpose |
210
+ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
211
+ | `iop-build-zip-archive` | Zip theme/plugin into `_builds/` |
212
+ | `iop-build-port-reporter` | Print discovered local WordPress URL/port (Docker) |
213
+ | `iop-project-refresh` | Sync host tooling from package boilerplate (`package.json` scripts, `docker-compose.yml`, `.gitignore`, dirs, optional templates). Flags: `--dry-run`, `--force` (allow dirty git tree) |
214
+ | `iop-html-php-prettier` | Experimental mixed HTML/PHP formatter |
215
+ | `iop-format-wp-block-pattern` | Experimental WordPress block-pattern PHP formatter |
216
+ | `iop-vscode-svgo` | SVGO over stdin for editor formatters |
217
+
218
+ ### `iop-html-php-prettier`
219
+
220
+ Double-formats mixed HTML and PHP (WordPress templates): PHP is tokenized, the file is formatted as HTML with Prettier, tokens are restored, then PHP is formatted with [@prettier/plugin-php](https://github.com/prettier/plugin-php).
221
+
222
+ A faster rewrite is in progress: **[Format Mixed PHP and HTML](https://github.com/ideasonpurpose/format-mixed-php-html)**.
223
+
224
+ ```sh
225
+ npx iop-html-php-prettier path/to/file.php
226
+ ```
227
+
228
+ Also accepts STDIN → STDOUT for Custom Local Formatters:
229
+
230
+ ```json
231
+ "customLocalFormatters.formatters": [
232
+ {
233
+ "command": "npx iop-html-php-prettier",
234
+ "languages": ["php"]
72
235
  }
236
+ ]
73
237
  ```
74
238
 
75
- Running a simple watch script to re-install on changes will make things somewhat seamless:
239
+ ### `iop-format-wp-block-pattern`
240
+
241
+ Formats WordPress block pattern PHP: readable block markup, expanded JSON in `<!-- wp:... -->` comments, and whitespace tuned for the block editor.
76
242
 
77
243
  ```sh
78
- cd dev-project-working-dir
79
- npm chokidar-cli "../../build-tools-wordpress/**/*" -c "npm install"
244
+ npx iop-format-wp-block-pattern path/to/pattern.php
80
245
  ```
81
246
 
82
- ### Additional Notes
247
+ ### `iop-project-refresh`
83
248
 
84
- This project expects an entirely ES Module based environment and specifies all dependencies using standard ESM import syntax. Projects importing this file should set `"type": "module"` in their package.json files.
249
+ Updates an existing project’s tooling from this package’s `boilerplate/` and `tooling/` trees. Safe defaults: refuses a dirty git working tree unless `--force`; use `--dry-run` to preview.
85
250
 
86
- #### Sass imports
251
+ ```sh
252
+ npx iop-project-refresh
253
+ npx iop-project-refresh --dry-run
254
+ npx iop-project-refresh --force
255
+ ```
87
256
 
88
- Sass can import stylesheet files directly from npm packages. Use the full path from node_modules like this:
257
+ ## Local development (this package)
89
258
 
90
- ```scss
91
- @use "@ideasonpurpose/build-tools-wordpress/example/package-style";
259
+ Bin scripts break conventional `npm link`. In a consumer project, depend on a local path instead:
260
+
261
+ ```json
262
+ "devDependencies": {
263
+ "@ideasonpurpose/build-tools-wordpress": "file:../../build-tools-wordpress"
264
+ }
265
+ ```
266
+
267
+ Reinstall on change:
268
+
269
+ ```sh
270
+ cd dev-project-working-dir
271
+ npx chokidar-cli "../../build-tools-wordpress/**/*" -c "npm install"
92
272
  ```
93
273
 
94
- #### Publishing to [npm](https://www.npmjs.com/package/@ideasonpurpose/build-tools-wordpress)
274
+ ## Publishing
275
+
276
+ Version-tagged releases publish to npm via GitHub Actions (OIDC trusted publishing). See [CHANGELOG.md](./CHANGELOG.md).
95
277
 
96
- A GitHub action will auto-publish version-tagged releases to npm. In order to publish, the repository must have an `NPM_TOKEN` secret set with the token from npm. [Log into npmjs.org](https://www.npmjs.com/login) with a publish-authorized account, then find the token page linked from the Profile page sidebar. Generate a new token and update the repository secret.
278
+ If the workflow needs a classic token instead, set repository secret `NPM_TOKEN` from an npm publish-capable account.
97
279
 
98
280
  <!-- START IOP CREDIT BLURB 2026-07-->
99
281
 
@@ -102,6 +284,6 @@ A GitHub action will auto-publish version-tagged releases to npm. In order to pu
102
284
  #### Brought to you by IOP
103
285
 
104
286
  | <a href="https://www.ideasonpurpose.com"><img src="https://raw.githubusercontent.com/ideasonpurpose/ideasonpurpose/master/iop-logo-white-on-black-88px.png" width="44" height="44" align="top" alt="IOP Logo"></a> <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | This project is actively developed and used in production at <a href="https://www.ideasonpurpose.com">Ideas On Purpose</a>. <br>&nbsp; |
105
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
287
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
106
288
 
107
289
  <!-- END IOP CREDIT BLURB -->
@@ -2,6 +2,9 @@
2
2
 
3
3
  //@ts-check
4
4
 
5
+ import prettierConfig from "@ideasonpurpose/prettier-config" with {
6
+ type: "json",
7
+ };
5
8
  /**
6
9
  * This is an experimental proof-of-concept for formatting mixed HTML & PHP
7
10
  * files from a single function.
@@ -9,15 +12,17 @@
9
12
  * TODO: Testing, naming, modularization, VS Code extension
10
13
  */
11
14
  import prettier from "prettier";
12
- import prettierConfig from "@ideasonpurpose/prettier-config" with { type: "json" };
13
15
 
14
16
  const phpPlugin = await import("@prettier/plugin-php");
15
17
 
16
18
  // Explicitly reset the plugin because global installs can't resolve it
19
+ // @ts-expect-error — prettierConfig types expect string paths, but runtime accepts plugin modules
17
20
  prettierConfig.plugins = [phpPlugin];
18
21
 
19
- import { readFile, writeFile } from "fs/promises";
20
- import { resolve, basename } from "path";
22
+ import { realpathSync } from "node:fs";
23
+ import { readFile, writeFile } from "node:fs/promises";
24
+ import { basename, resolve } from "node:path";
25
+ import { fileURLToPath } from "node:url";
21
26
 
22
27
  /**
23
28
  * Prettier API doesn't recognize overrides, so we extract them
@@ -30,6 +35,7 @@ const phpOptions = prettierConfig.overrides.find(
30
35
  (o) => o.files === "*.php",
31
36
  )?.options;
32
37
 
38
+ /** @param {string} html @param {number} offset */
33
39
  const isInTag = (html, offset) => {
34
40
  if (offset === 0) return false;
35
41
 
@@ -56,6 +62,7 @@ const isInTag = (html, offset) => {
56
62
  * being mutilated by the HTML formatting step. Cleaner than adding a string.replace
57
63
  * to unTokenizeHTML().
58
64
  */
65
+ /** @param {string} htmlContent */
59
66
  export function tokenizeHTML(htmlContent) {
60
67
  let tokenizedHTML = "";
61
68
  const phpCodeBlocks = new Map(); // Changed to Map for better performance and type safety
@@ -67,6 +74,7 @@ export function tokenizeHTML(htmlContent) {
67
74
  *
68
75
  * NOTE: This uses tokenCount from the enclosing scope
69
76
  */
77
+ /** @param {string} phpCodeBlock @param {string} prevContent */
70
78
  const tokenizeCodeBlock = (phpCodeBlock, prevContent) => {
71
79
  let start = "<";
72
80
  let end = " />";
@@ -79,27 +87,14 @@ export function tokenizeHTML(htmlContent) {
79
87
  return `${start}php_${tokenCount++}__`.padEnd(codeLength, "_") + end;
80
88
  };
81
89
 
82
- // const pattern = /<\?(?:php|=)[\s\S]*?\?>/gs;
83
- // const pattern =
84
- // /(?<before>(?:[^\s]|\s|^)\s*)(?<php><\?(?:php|=).*?(?:\?>|$))(?<after>(?:\s*)[^\s]|$)/gs;
85
- // const pattern =
86
- // /((?:[^\s]|\s|^)\s*)(<\?(?:php|=).*?(?:\?>|$))((?:\s*)[^\s]|$)/gms;
87
- // // const pattern = /([^\s]+)\s*(<\?(?:php|=).*?(?:\?>|$))\s*([^\s]*)/gms;
88
- // const pattern =
89
- // /([^\s]?\s*)?(<\?(?:php|=).*?(?:\?>|$))((?:\s*)[^\s]|$)/gms;
90
- // const pattern =
91
- // /(?<=((?:[^\s]|\s|^)\s*))(<\?(?:php|=).*?\?>)(?=((?:\s*)[^\s]|$))/gms;
92
- // try removing look ahead/behind
93
- const pattern = /(<\?(?:php|=).*?\?>)/gms;
94
-
95
90
  // const regex = new RegExp(/<\?(?:php|=).*?\?>/, "gs");
96
91
  // Trying to capture open-ended PHP codeBlocks in a single regexp
97
92
  const regex = new RegExp(/<\?(?:php|=).*?(?:\?>|$)/, "gs");
98
93
 
99
- let match;
94
+ let match = regex.exec(htmlContent);
100
95
  let token;
101
96
  let lastIndex = 0;
102
- while ((match = regex.exec(htmlContent)) !== null) {
97
+ while (match !== null) {
103
98
  tokenizedHTML += htmlContent.slice(lastIndex, match.index);
104
99
 
105
100
  token = tokenizeCodeBlock(match[0], tokenizedHTML);
@@ -107,12 +102,14 @@ export function tokenizeHTML(htmlContent) {
107
102
  tokenizedHTML += token;
108
103
 
109
104
  lastIndex = match.index + match[0].length;
105
+ match = regex.exec(htmlContent);
110
106
  }
111
107
  tokenizedHTML += htmlContent.slice(lastIndex);
112
108
 
113
109
  return { tokenizedHTML, phpCodeBlocks };
114
110
  }
115
111
 
112
+ /** @param {string} tokenizedHTML @param {Map<string, string>} phpCodeBlocks */
116
113
  export function unTokenizeHTML(tokenizedHTML, phpCodeBlocks) {
117
114
  let phpContent = tokenizedHTML;
118
115
  for (const [token, phpBlock] of phpCodeBlocks) {
@@ -132,55 +129,129 @@ export function unTokenizeHTML(tokenizedHTML, phpCodeBlocks) {
132
129
  }
133
130
 
134
131
  /**
135
- * Formats a mixed HTML & PHP file with these steps:
132
+ * @param {bigint} start
133
+ * @param {bigint} end
134
+ */
135
+ const ms = (start, end) => Number(end - start) / 1e6;
136
+
137
+ /**
138
+ * Formats mixed HTML & PHP content:
136
139
  * 1. Tokenize PHP Blocks as HTML-safe and attribute-safe strings
137
140
  * 2. Format the result as HTML
138
141
  * 3. Un-tokenize HTML back to PHP
139
142
  * 4. Format again as PHP
140
- * 5. Overwrite the file
141
143
  *
142
- * @param {string} filepath - The path to the file to format (must be a valid file path).
144
+ * @param {string} content
145
+ * @param {string} [label]
146
+ * @returns {Promise<string>}
143
147
  */
144
- export async function formatHTMLThenPHP(filepath) {
145
- try {
146
- const startTime = process.hrtime.bigint();
147
- const rawFile = await readFile(filepath, "utf8");
148
+ export async function formatHTMLThenPHPContent(content, label = "stdin") {
149
+ const t0 = process.hrtime.bigint();
150
+ const startupMs = process.uptime() * 1e3;
148
151
 
149
- const { tokenizedHTML, phpCodeBlocks } = tokenizeHTML(rawFile);
152
+ const tTokenize0 = process.hrtime.bigint();
153
+ const { tokenizedHTML, phpCodeBlocks } = tokenizeHTML(content);
154
+ const tTokenize1 = process.hrtime.bigint();
150
155
 
151
- const htmlFormatted = await prettier.format(tokenizedHTML, {
152
- ...prettierConfig,
153
- ...htmlOptions,
154
- parser: "html",
155
- embeddedLanguageFormatting: "auto",
156
- });
156
+ const htmlFormatted = await prettier.format(tokenizedHTML, {
157
+ ...prettierConfig,
158
+ ...htmlOptions,
159
+ parser: "html",
160
+ embeddedLanguageFormatting: "auto",
161
+ });
162
+ const tHtml = process.hrtime.bigint();
157
163
 
158
- const phpUnTokenized = unTokenizeHTML(htmlFormatted, phpCodeBlocks);
164
+ const phpUnTokenized = unTokenizeHTML(htmlFormatted, phpCodeBlocks);
165
+ const tUnTokenize = process.hrtime.bigint();
159
166
 
160
- const phpFormatted = await prettier.format(phpUnTokenized, {
161
- ...prettierConfig,
162
- ...phpOptions,
163
- parser: "php",
164
- embeddedLanguageFormatting: "auto",
165
- // plugins: [phpPlugin], // Explicitly pass the plugin
166
- });
167
+ const phpFormatted = await prettier.format(phpUnTokenized, {
168
+ ...prettierConfig,
169
+ ...phpOptions,
170
+ parser: "php",
171
+ embeddedLanguageFormatting: "auto",
172
+ });
173
+ const tEnd = process.hrtime.bigint();
167
174
 
168
- await writeFile(filepath, phpFormatted, "utf8");
169
- const endTime = process.hrtime.bigint();
170
- const duration = Number(endTime - startTime);
175
+ console.error(
176
+ [
177
+ label,
178
+ `startup ${startupMs.toFixed(2)}ms`,
179
+ `tokenize ${ms(tTokenize0, tTokenize1).toFixed(2)}ms`,
180
+ `formatHTML ${ms(tTokenize1, tHtml).toFixed(2)}ms`,
181
+ `unTokenize ${ms(tHtml, tUnTokenize).toFixed(2)}ms`,
182
+ `formatPHP ${ms(tUnTokenize, tEnd).toFixed(2)}ms`,
183
+ `total ${(startupMs + ms(t0, tEnd)).toFixed(2)}ms`,
184
+ ].join(" "),
185
+ );
171
186
 
172
- console.log(`${basename(filepath)} ${(duration / 1e6).toFixed(2)}ms`);
173
- } catch (error) {
174
- console.error("Error:", error);
187
+ return phpFormatted;
188
+ }
189
+
190
+ /**
191
+ * Formats a mixed HTML & PHP file in place.
192
+ *
193
+ * @param {string} filepath
194
+ */
195
+ export async function formatHTMLThenPHP(filepath) {
196
+ const rawFile = await readFile(filepath, "utf8");
197
+ const phpFormatted = await formatHTMLThenPHPContent(
198
+ rawFile,
199
+ basename(filepath),
200
+ );
201
+ await writeFile(filepath, phpFormatted, "utf8");
202
+ }
203
+
204
+ /**
205
+ * @returns {Promise<string>}
206
+ */
207
+ async function readStdin() {
208
+ const chunks = [];
209
+ for await (const chunk of process.stdin) {
210
+ chunks.push(chunk);
175
211
  }
212
+ return Buffer.concat(chunks).toString("utf8");
176
213
  }
177
214
 
215
+ /**
216
+ * CLI: filepath arg overwrites the file; no arg reads STDIN and writes STDOUT.
217
+ *
218
+ * @param {string} [filepath]
219
+ */
178
220
  export async function main(filepath = process.argv[2]) {
179
- if (!filepath) {
180
- console.error("Error: A filepath is required.");
181
- return;
221
+ try {
222
+ if (filepath) {
223
+ await formatHTMLThenPHP(resolve(filepath));
224
+ return;
225
+ }
226
+
227
+ if (process.stdin.isTTY) {
228
+ console.error(
229
+ "Usage: iop-html-php-prettier <filepath>\n iop-html-php-prettier < input.php",
230
+ );
231
+ process.exitCode = 1;
232
+ return;
233
+ }
234
+
235
+ const formatted = await formatHTMLThenPHPContent(
236
+ await readStdin(),
237
+ "stdin",
238
+ );
239
+ process.stdout.write(formatted);
240
+ } catch (error) {
241
+ console.error("Error:", error);
242
+ process.exitCode = 1;
182
243
  }
183
- await formatHTMLThenPHP(resolve(filepath));
184
244
  }
185
245
 
186
- if (process.argv[2]) main();
246
+ const isMain = (() => {
247
+ if (process.argv[1] == null) return false;
248
+ try {
249
+ return (
250
+ fileURLToPath(import.meta.url) === realpathSync(resolve(process.argv[1]))
251
+ );
252
+ } catch {
253
+ return false;
254
+ }
255
+ })();
256
+
257
+ if (isMain) main();