@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 +232 -50
- package/bin/format-php-prettier.js +122 -51
- package/bin/format-wp-block-pattern.js +62 -15
- package/bin/format-wp-php.js +83 -0
- package/bin/vscode-svgo.js +32 -0
- package/biome.json +43 -0
- package/package.json +6 -6
- package/CHANGELOG.md +0 -520
- /package/{boilerplate → config}/svgo.config.mjs +0 -0
package/README.md
CHANGED
|
@@ -1,99 +1,281 @@
|
|
|
1
1
|
# @ideasonpurpose/build-tools-wordpress
|
|
2
2
|
|
|
3
|
-
#### Version 2.10.
|
|
3
|
+
#### Version 2.10.8
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@ideasonpurpose/build-tools-wordpress)
|
|
6
|
-
[](https://github.com/ideasonpurpose/build-tools-wordpress
|
|
6
|
+
[](https://github.com/ideasonpurpose/build-tools-wordpress/actions/workflows/npm-publish.yml)
|
|
7
|
+
[](./LICENSE)
|
|
7
8
|
|
|
8
|
-
Build scripts and dependencies for
|
|
9
|
+
Build scripts and shared dependencies for WordPress development. Used in production at [Ideas On Purpose](https://www.ideasonpurpose.com).
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
|
|
13
|
+
## Requirements
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
- **Node.js** 22.15+ (required by webpack-dev-server 6; ESM only)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
## Install
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
```sh
|
|
20
|
+
npm install -D @ideasonpurpose/build-tools-wordpress
|
|
21
|
+
```
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
Typical host scripts (see [`boilerplate/package.json`](./boilerplate/package.json)):
|
|
21
24
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
```js
|
|
52
|
+
// webpack.config.js
|
|
53
|
+
export { webpackConfig as default } from "@ideasonpurpose/build-tools-wordpress";
|
|
54
|
+
```
|
|
33
55
|
|
|
34
|
-
|
|
56
|
+
**2. Lay out sources** under the default theme paths (package `name` → theme folder):
|
|
35
57
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
## Configuration
|
|
48
78
|
|
|
49
|
-
|
|
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
|
-
|
|
81
|
+
### Defaults
|
|
52
82
|
|
|
53
|
-
|
|
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
|
-
|
|
95
|
+
Paths resolve relative to the config file (or `package.json` if no config file is found).
|
|
56
96
|
|
|
57
|
-
|
|
58
|
-
**Usage:** `npx iop-html-php-prettier path/to/file.php`
|
|
97
|
+
### Entry shapes
|
|
59
98
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
+
### SVGO and VS Code
|
|
66
172
|
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
247
|
+
### `iop-project-refresh`
|
|
83
248
|
|
|
84
|
-
|
|
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
|
-
|
|
251
|
+
```sh
|
|
252
|
+
npx iop-project-refresh
|
|
253
|
+
npx iop-project-refresh --dry-run
|
|
254
|
+
npx iop-project-refresh --force
|
|
255
|
+
```
|
|
87
256
|
|
|
88
|
-
|
|
257
|
+
## Local development (this package)
|
|
89
258
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
274
|
+
## Publishing
|
|
275
|
+
|
|
276
|
+
Version-tagged releases publish to npm via GitHub Actions (OIDC trusted publishing). See [CHANGELOG.md](./CHANGELOG.md).
|
|
95
277
|
|
|
96
|
-
|
|
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> | This project is actively developed and used in production at <a href="https://www.ideasonpurpose.com">Ideas On Purpose</a>. <br> |
|
|
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 {
|
|
20
|
-
import {
|
|
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 (
|
|
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
|
-
*
|
|
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}
|
|
144
|
+
* @param {string} content
|
|
145
|
+
* @param {string} [label]
|
|
146
|
+
* @returns {Promise<string>}
|
|
143
147
|
*/
|
|
144
|
-
export async function
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
152
|
+
const tTokenize0 = process.hrtime.bigint();
|
|
153
|
+
const { tokenizedHTML, phpCodeBlocks } = tokenizeHTML(content);
|
|
154
|
+
const tTokenize1 = process.hrtime.bigint();
|
|
150
155
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
164
|
+
const phpUnTokenized = unTokenizeHTML(htmlFormatted, phpCodeBlocks);
|
|
165
|
+
const tUnTokenize = process.hrtime.bigint();
|
|
159
166
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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();
|