├── bin ├── wp-hooks-generator └── wp-hooks-validator ├── composer.json ├── interface └── index.d.ts ├── package-lock.json ├── package.json ├── readme.md ├── schema.json └── src ├── generate.php └── validate.php /bin/wp-hooks-generator: -------------------------------------------------------------------------------- 1 | ../src/generate.php -------------------------------------------------------------------------------- /bin/wp-hooks-validator: -------------------------------------------------------------------------------- 1 | ../src/validate.php -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-hooks/generator", 3 | "description": "Generates a JSON representation of the WordPress actions and filters in your code", 4 | "type": "library", 5 | "license": "GPL-3.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "John Blackbourn", 9 | "homepage": "https://johnblackbourn.com/" 10 | } 11 | ], 12 | "config": { 13 | "sort-packages": true, 14 | "allow-plugins": { 15 | "composer/installers": true, 16 | "oomphinc/composer-installers-extender": true 17 | } 18 | }, 19 | "bin": [ 20 | "bin/wp-hooks-generator" 21 | ], 22 | "require": { 23 | "php": ">=8.3", 24 | "ext-libxml": "*", 25 | "erusev/parsedown": "1.8.0-beta-7", 26 | "nikic/php-parser": "5.3.1", 27 | "phpdocumentor/reflection-docblock": "5.5.1" 28 | }, 29 | "require-dev": { 30 | "oomphinc/composer-installers-extender": "^2", 31 | "opis/json-schema": "2.3.0" 32 | }, 33 | "funding": [ 34 | { 35 | "type": "github", 36 | "url": "https://github.com/sponsors/johnbillion" 37 | } 38 | ], 39 | "replace": { 40 | "johnbillion/wp-hooks-generator": "*" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /interface/index.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /** 3 | * This file was automatically generated by json-schema-to-typescript. 4 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 5 | * and run json-schema-to-typescript to regenerate this file. 6 | */ 7 | 8 | /** 9 | * The docblock tags for the hook 10 | */ 11 | export type Tags = Tag[]; 12 | /** 13 | * The list of hooks 14 | */ 15 | export type Hooks = Hook[]; 16 | 17 | /** 18 | * The container for the list of hooks 19 | */ 20 | export interface HooksContainer { 21 | /** 22 | * The JSON schema to verify a hook document against 23 | */ 24 | $schema: string; 25 | hooks: Hooks; 26 | } 27 | /** 28 | * The hook representation 29 | */ 30 | export interface Hook { 31 | /** 32 | * The hook name 33 | */ 34 | name: string; 35 | /** 36 | * Aliases of the hook name 37 | */ 38 | aliases?: string[]; 39 | /** 40 | * The relative name of the file containing the hook 41 | */ 42 | file: string; 43 | /** 44 | * The hook type 45 | */ 46 | type: string; 47 | doc: Doc; 48 | /** 49 | * The number of arguments passed to the hook 50 | */ 51 | args: number; 52 | } 53 | /** 54 | * The docblock information for the hook 55 | */ 56 | export interface Doc { 57 | /** 58 | * The short description as plain text 59 | */ 60 | description: string; 61 | /** 62 | * The long description as markdown 63 | */ 64 | long_description: string; 65 | /** 66 | * The long description as HTML 67 | */ 68 | long_description_html: string; 69 | tags: Tags; 70 | } 71 | /** 72 | * The docblock tags information for the hook 73 | */ 74 | export interface Tag { 75 | /** 76 | * The tag name 77 | */ 78 | name: string; 79 | /** 80 | * The tag content 81 | */ 82 | content: string; 83 | /** 84 | * Allowed types for parameter values, for @param tags 85 | */ 86 | types?: string[]; 87 | /** 88 | * The name of the parameter variable, for @param tags 89 | */ 90 | variable?: string; 91 | /** 92 | * A link to more information, for @link tags 93 | */ 94 | link?: string; 95 | /** 96 | * Related function to refer to, for @see tags 97 | */ 98 | refers?: string; 99 | /** 100 | * This is only used for @since 3.0.0 MU tags 101 | */ 102 | description?: string; 103 | } 104 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-hooks/generator", 3 | "version": "1.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/json-schema": { 8 | "version": "7.0.3", 9 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", 10 | "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==" 11 | }, 12 | "@types/node": { 13 | "version": "12.12.7", 14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.7.tgz", 15 | "integrity": "sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==" 16 | }, 17 | "@types/prettier": { 18 | "version": "1.18.3", 19 | "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.18.3.tgz", 20 | "integrity": "sha512-48rnerQdcZ26odp+HOvDGX8IcUkYOCuMc2BodWYTe956MqkHlOGAG4oFQ83cjZ0a4GAgj7mb4GUClxYd2Hlodg==" 21 | }, 22 | "ansi-regex": { 23 | "version": "2.1.1", 24 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 25 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 26 | }, 27 | "any-promise": { 28 | "version": "1.3.0", 29 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 30 | "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" 31 | }, 32 | "argparse": { 33 | "version": "1.0.10", 34 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 35 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 36 | "requires": { 37 | "sprintf-js": "~1.0.2" 38 | } 39 | }, 40 | "call-me-maybe": { 41 | "version": "1.0.1", 42 | "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", 43 | "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" 44 | }, 45 | "cli-color": { 46 | "version": "1.4.0", 47 | "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz", 48 | "integrity": "sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==", 49 | "requires": { 50 | "ansi-regex": "^2.1.1", 51 | "d": "1", 52 | "es5-ext": "^0.10.46", 53 | "es6-iterator": "^2.0.3", 54 | "memoizee": "^0.4.14", 55 | "timers-ext": "^0.1.5" 56 | } 57 | }, 58 | "d": { 59 | "version": "1.0.1", 60 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", 61 | "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", 62 | "requires": { 63 | "es5-ext": "^0.10.50", 64 | "type": "^1.0.1" 65 | } 66 | }, 67 | "es5-ext": { 68 | "version": "0.10.52", 69 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.52.tgz", 70 | "integrity": "sha512-bWCbE9fbpYQY4CU6hJbJ1vSz70EClMlDgJ7BmwI+zEJhxrwjesZRPglGJlsZhu0334U3hI+gaspwksH9IGD6ag==", 71 | "requires": { 72 | "es6-iterator": "~2.0.3", 73 | "es6-symbol": "~3.1.2", 74 | "next-tick": "~1.0.0" 75 | } 76 | }, 77 | "es6-iterator": { 78 | "version": "2.0.3", 79 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", 80 | "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", 81 | "requires": { 82 | "d": "1", 83 | "es5-ext": "^0.10.35", 84 | "es6-symbol": "^3.1.1" 85 | } 86 | }, 87 | "es6-symbol": { 88 | "version": "3.1.3", 89 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", 90 | "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", 91 | "requires": { 92 | "d": "^1.0.1", 93 | "ext": "^1.1.2" 94 | } 95 | }, 96 | "es6-weak-map": { 97 | "version": "2.0.3", 98 | "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", 99 | "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", 100 | "requires": { 101 | "d": "1", 102 | "es5-ext": "^0.10.46", 103 | "es6-iterator": "^2.0.3", 104 | "es6-symbol": "^3.1.1" 105 | } 106 | }, 107 | "esprima": { 108 | "version": "4.0.1", 109 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 110 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" 111 | }, 112 | "event-emitter": { 113 | "version": "0.3.5", 114 | "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", 115 | "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", 116 | "requires": { 117 | "d": "1", 118 | "es5-ext": "~0.10.14" 119 | } 120 | }, 121 | "ext": { 122 | "version": "1.2.0", 123 | "resolved": "https://registry.npmjs.org/ext/-/ext-1.2.0.tgz", 124 | "integrity": "sha512-0ccUQK/9e3NreLFg6K6np8aPyRgwycx+oFGtfx1dSp7Wj00Ozw9r05FgBRlzjf2XBM7LAzwgLyDscRrtSU91hA==", 125 | "requires": { 126 | "type": "^2.0.0" 127 | }, 128 | "dependencies": { 129 | "type": { 130 | "version": "2.0.0", 131 | "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", 132 | "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==" 133 | } 134 | } 135 | }, 136 | "format-util": { 137 | "version": "1.0.3", 138 | "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.3.tgz", 139 | "integrity": "sha1-Ay3KShFiYqEsQ/TD7IVmQWxbLZU=" 140 | }, 141 | "is-promise": { 142 | "version": "2.1.0", 143 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 144 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" 145 | }, 146 | "js-yaml": { 147 | "version": "3.13.1", 148 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 149 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 150 | "requires": { 151 | "argparse": "^1.0.7", 152 | "esprima": "^4.0.0" 153 | } 154 | }, 155 | "json-schema-ref-parser": { 156 | "version": "6.1.0", 157 | "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz", 158 | "integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==", 159 | "requires": { 160 | "call-me-maybe": "^1.0.1", 161 | "js-yaml": "^3.12.1", 162 | "ono": "^4.0.11" 163 | } 164 | }, 165 | "json-schema-to-typescript": { 166 | "version": "7.1.0", 167 | "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-7.1.0.tgz", 168 | "integrity": "sha512-7tQyQzR+0NyI2iPjkqLLe4ncaxov1oIAFRM+BI8DDs7wxVIXsd0GWoZJIPsNQePIDr7N/LCkYtkEEDvY7dhGzA==", 169 | "requires": { 170 | "@types/json-schema": "^7.0.3", 171 | "@types/node": ">=4.5.0", 172 | "@types/prettier": "^1.16.1", 173 | "cli-color": "^1.4.0", 174 | "json-schema-ref-parser": "^6.1.0", 175 | "json-stringify-safe": "^5.0.1", 176 | "lodash": "^4.17.11", 177 | "minimist": "^1.2.0", 178 | "mz": "^2.7.0", 179 | "prettier": "^1.18.2", 180 | "stdin": "0.0.1" 181 | } 182 | }, 183 | "json-stringify-safe": { 184 | "version": "5.0.1", 185 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 186 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 187 | }, 188 | "lodash": { 189 | "version": "4.17.15", 190 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 191 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 192 | }, 193 | "lru-queue": { 194 | "version": "0.1.0", 195 | "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", 196 | "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", 197 | "requires": { 198 | "es5-ext": "~0.10.2" 199 | } 200 | }, 201 | "memoizee": { 202 | "version": "0.4.14", 203 | "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", 204 | "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==", 205 | "requires": { 206 | "d": "1", 207 | "es5-ext": "^0.10.45", 208 | "es6-weak-map": "^2.0.2", 209 | "event-emitter": "^0.3.5", 210 | "is-promise": "^2.1", 211 | "lru-queue": "0.1", 212 | "next-tick": "1", 213 | "timers-ext": "^0.1.5" 214 | } 215 | }, 216 | "minimist": { 217 | "version": "1.2.0", 218 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 219 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 220 | }, 221 | "mz": { 222 | "version": "2.7.0", 223 | "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 224 | "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 225 | "requires": { 226 | "any-promise": "^1.0.0", 227 | "object-assign": "^4.0.1", 228 | "thenify-all": "^1.0.0" 229 | } 230 | }, 231 | "next-tick": { 232 | "version": "1.0.0", 233 | "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", 234 | "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" 235 | }, 236 | "object-assign": { 237 | "version": "4.1.1", 238 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 239 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 240 | }, 241 | "ono": { 242 | "version": "4.0.11", 243 | "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", 244 | "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", 245 | "requires": { 246 | "format-util": "^1.0.3" 247 | } 248 | }, 249 | "prettier": { 250 | "version": "1.19.1", 251 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", 252 | "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==" 253 | }, 254 | "sprintf-js": { 255 | "version": "1.0.3", 256 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 257 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" 258 | }, 259 | "stdin": { 260 | "version": "0.0.1", 261 | "resolved": "https://registry.npmjs.org/stdin/-/stdin-0.0.1.tgz", 262 | "integrity": "sha1-0wQZgarsPf28d6GzjWNy449ftx4=" 263 | }, 264 | "thenify": { 265 | "version": "3.3.0", 266 | "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", 267 | "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", 268 | "requires": { 269 | "any-promise": "^1.0.0" 270 | } 271 | }, 272 | "thenify-all": { 273 | "version": "1.6.0", 274 | "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 275 | "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", 276 | "requires": { 277 | "thenify": ">= 3.1.0 < 4" 278 | } 279 | }, 280 | "timers-ext": { 281 | "version": "0.1.7", 282 | "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", 283 | "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", 284 | "requires": { 285 | "es5-ext": "~0.10.46", 286 | "next-tick": "1" 287 | } 288 | }, 289 | "type": { 290 | "version": "1.2.0", 291 | "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", 292 | "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@johnbillion/wp-hooks-generator", 3 | "version": "1.0.1", 4 | "description": "Generates a JSON representation of the WordPress actions and filters in your code", 5 | "private": true, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/wp-hooks/generator.git" 9 | }, 10 | "author": "John Blackbourn", 11 | "license": "GPL-3.0-or-later", 12 | "bugs": { 13 | "url": "https://github.com/wp-hooks/generator/issues" 14 | }, 15 | "dependencies": { 16 | "json-schema-to-typescript": "^7.1" 17 | }, 18 | "scripts": { 19 | "generate-interfaces": "json2ts --input schema.json --output=interface/index.d.ts" 20 | }, 21 | "homepage": "https://github.com/wp-hooks/generator#readme" 22 | } 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # WP Hooks Generator 2 | 3 | Generates a JSON representation of the WordPress actions and filters in your code. Can be used with WordPress plugins, themes, and core. 4 | 5 | Note: If you just want the hook files without generating them yourself, use the following packages instead: 6 | 7 | * [wp-hooks/wordpress-core](https://github.com/wp-hooks/wordpress-core) for WordPress core 8 | 9 | ## Requirements 10 | 11 | PHP 8.3 or higher. 12 | 13 | ## Installation 14 | 15 | ```shell 16 | composer require wp-hooks/generator 17 | ``` 18 | 19 | ## Generating the Hook Files 20 | 21 | ```shell 22 | ./bin/wp-hooks-generator --input=src --output=hooks 23 | ``` 24 | 25 | ## Usage of the Generated Hook Files in PHP 26 | 27 | ```php 28 | // Get hooks as JSON: 29 | $actions_json = file_get_contents( 'hooks/actions.json' ); 30 | $filters_json = file_get_contents( 'hooks/filters.json' ); 31 | 32 | // Convert hooks to PHP: 33 | $actions = json_decode( $actions_json, true )['hooks']; 34 | $filters = json_decode( $filters_json, true )['hooks']; 35 | 36 | // Search for filters matching a string: 37 | $search = 'permalink'; 38 | $results = array_filter( $filters, function( array $hook ) use ( $search ) { 39 | return ( strpos( $hook['name'], $search ) !== false ); 40 | } ); 41 | 42 | var_dump( $results ); 43 | ``` 44 | 45 | ## Usage of the Generated Hook Files in JavaScript 46 | 47 | ```js 48 | // Get hooks as array of objects: 49 | const actions = require('hooks/actions.json').hooks; 50 | const filters = require('hooks/filters.json').hooks; 51 | 52 | // Search for actions matching a string: 53 | const search = 'menu'; 54 | const results = actions.filter( hook => ( hook.name.match( search ) !== null ) ); 55 | 56 | console.log(results); 57 | ``` 58 | 59 | ## Ignoring Files or Directories 60 | 61 | You can ignore files or directories in two ways: 62 | 63 | ### On the Command Line 64 | 65 | ./vendor/bin/wp-hooks-generator --input=src --output=hooks --ignore-files="ignore/this,ignore/that" 66 | 67 | ### In composer.json 68 | 69 | ```json 70 | "extra": { 71 | "wp-hooks": { 72 | "ignore-files": [ 73 | "ignore/this", 74 | "ignore/that" 75 | ] 76 | } 77 | } 78 | ``` 79 | 80 | ## Ignoring Hooks 81 | 82 | You can ignore hooks in two ways: 83 | 84 | ### On the Command Line 85 | 86 | ./vendor/bin/wp-hooks-generator --input=src --output=hooks --ignore-hooks="this_hook,that_hook" 87 | 88 | ### In composer.json 89 | 90 | ```json 91 | "extra": { 92 | "wp-hooks": { 93 | "ignore-hooks": [ 94 | "this_hook", 95 | "that_hook" 96 | ] 97 | } 98 | } 99 | ``` 100 | 101 | ## TypeScript Interfaces for the Hook Files 102 | 103 | The TypeScript interfaces for the hook files can be found in [`interface/index.d.ts`](interface/index.d.ts). Usage: 104 | 105 | ```typescript 106 | import { Hooks, Hook, Doc, Tags, Tag } from 'hooks/index.d.ts'; 107 | ``` 108 | 109 | ## JSON Schema for the Hook Files 110 | 111 | The JSON schema for the hook files can be found in [`schema.json`](schema.json). 112 | -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://github.com/wp-hooks/generator/blob/1.0.1/schema.json", 4 | "title": "HooksContainer", 5 | "description": "The container for the list of hooks", 6 | "type": "object", 7 | "required": [ 8 | "$schema", 9 | "hooks" 10 | ], 11 | "additionalProperties": false, 12 | "properties": { 13 | "$schema": { 14 | "type": "string", 15 | "description": "The JSON schema to verify a hook document against", 16 | "format": "uri" 17 | }, 18 | "hooks": { 19 | "type": "array", 20 | "title": "Hooks", 21 | "description": "The list of hooks", 22 | "items": { 23 | "title": "Hook", 24 | "description": "The hook representation", 25 | "type": "object", 26 | "additionalProperties": false, 27 | "required": [ 28 | "name", 29 | "file", 30 | "type", 31 | "doc", 32 | "args" 33 | ], 34 | "properties": { 35 | "name": { 36 | "description": "The hook name", 37 | "type": "string", 38 | "examples": [ 39 | "attachment_fields_to_edit", 40 | "update_site_option_{$option}", 41 | "init" 42 | ] 43 | }, 44 | "aliases": { 45 | "description": "Aliases of the hook name", 46 | "type": "array", 47 | "items": { 48 | "type": "string" 49 | } 50 | }, 51 | "file": { 52 | "description": "The relative name of the file containing the hook", 53 | "type": "string", 54 | "examples": [ 55 | "wp-admin/includes/menu.php" 56 | ] 57 | }, 58 | "type": { 59 | "description": "The hook type", 60 | "type": "string", 61 | "examples": [ 62 | "action", 63 | "filter", 64 | "action_reference", 65 | "filter_reference" 66 | ] 67 | }, 68 | "doc": { 69 | "title": "Doc", 70 | "description": "The docblock information for the hook", 71 | "type": "object", 72 | "additionalProperties": false, 73 | "required": [ 74 | "description", 75 | "long_description", 76 | "long_description_html", 77 | "tags" 78 | ], 79 | "properties": { 80 | "description": { 81 | "description": "The short description as plain text", 82 | "type": "string" 83 | }, 84 | "long_description": { 85 | "description": "The long description as markdown", 86 | "type": "string" 87 | }, 88 | "long_description_html": { 89 | "description": "The long description as HTML", 90 | "type": "string" 91 | }, 92 | "tags": { 93 | "title": "Tags", 94 | "description": "The docblock tags for the hook", 95 | "type": "array", 96 | "items": { 97 | "title": "Tag", 98 | "description": "The docblock tags information for the hook", 99 | "type": "object", 100 | "additionalProperties": false, 101 | "required": [ 102 | "name", 103 | "content" 104 | ], 105 | "properties": { 106 | "name": { 107 | "description": "The tag name", 108 | "type": "string", 109 | "examples": [ 110 | "deprecated", 111 | "global", 112 | "ignore", 113 | "link", 114 | "param", 115 | "private", 116 | "return", 117 | "see", 118 | "since", 119 | "todo" 120 | ] 121 | }, 122 | "content": { 123 | "description": "The tag content", 124 | "type": "string", 125 | "examples": [ 126 | "Name of the network option.", 127 | "5.2.0" 128 | ] 129 | }, 130 | "types": { 131 | "description": "Allowed types for parameter values, for @param tags", 132 | "type": "array", 133 | "items": { 134 | "type": "string" 135 | } 136 | }, 137 | "variable": { 138 | "description": "The name of the parameter variable, for @param tags", 139 | "type": "string", 140 | "examples": [ 141 | "$args", 142 | "$option" 143 | ] 144 | }, 145 | "link": { 146 | "description": "A link to more information, for @link tags", 147 | "type": "string", 148 | "format": "uri", 149 | "examples": [ 150 | "https://core.trac.wordpress.org/ticket/19321" 151 | ] 152 | }, 153 | "refers": { 154 | "description": "Related function to refer to, for @see tags", 155 | "type": "string", 156 | "examples": [ 157 | "wp_delete_post()" 158 | ] 159 | }, 160 | "description": { 161 | "description": "This is only used for @since 3.0.0 MU tags", 162 | "type": "string" 163 | } 164 | } 165 | } 166 | } 167 | } 168 | }, 169 | "args": { 170 | "description": "The number of arguments passed to the hook", 171 | "type": "integer" 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/generate.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | extra ) && ! empty( $config->extra->{"wp-hooks"} ) ) { 46 | // Read ignore-files from Composer config: 47 | if ( empty( $options['ignore-files'] ) && ! empty( $config->extra->{"wp-hooks"}->{"ignore-files"} ) ) { 48 | $options['ignore-files'] = array_values( $config->extra->{"wp-hooks"}->{"ignore-files"} ); 49 | } 50 | 51 | // Read ignore-hooks from Composer config: 52 | if ( empty( $options['ignore-hooks'] ) && ! empty( $config->extra->{"wp-hooks"}->{"ignore-hooks"} ) ) { 53 | $options['ignore-hooks'] = array_values( $config->extra->{"wp-hooks"}->{"ignore-hooks"} ); 54 | } 55 | } 56 | 57 | if ( empty( $options['ignore-files'] ) ) { 58 | $options['ignore-files'] = []; 59 | } 60 | 61 | if ( empty( $options['ignore-hooks'] ) ) { 62 | $options['ignore-hooks'] = []; 63 | } 64 | 65 | $source_dir = $options['input']; 66 | $target_dir = $options['output']; 67 | $ignore_files = $options['ignore-files']; 68 | $ignore_hooks = $options['ignore-hooks']; 69 | 70 | if ( ! file_exists( $source_dir ) ) { 71 | printf( 72 | 'The source directory "%s" does not exist.' . "\n", 73 | $source_dir 74 | ); 75 | exit( 1 ); 76 | } 77 | 78 | if ( ! file_exists( $target_dir ) ) { 79 | printf( 80 | 'The target directory "%s" does not exist. Please create it first.' . "\n", 81 | $target_dir 82 | ); 83 | exit( 1 ); 84 | } 85 | 86 | echo "Scanning for files...\n"; 87 | 88 | /** 89 | * @param string $directory 90 | * 91 | * @return array|\WP_Error 92 | */ 93 | function get_wp_files( $directory ) { 94 | $iterableFiles = new \RecursiveIteratorIterator( 95 | new \RecursiveDirectoryIterator( $directory ) 96 | ); 97 | $files = array(); 98 | 99 | foreach ( $iterableFiles as $file ) { 100 | if ( 'php' !== $file->getExtension() ) { 101 | continue; 102 | } 103 | 104 | $files[] = $file->getPathname(); 105 | } 106 | 107 | return $files; 108 | } 109 | 110 | /** @var array */ 111 | $files = get_wp_files( $source_dir ); 112 | $files = array_values( array_filter( $files, function( string $file ) use ( $ignore_files ) : bool { 113 | foreach ( $ignore_files as $i ) { 114 | if ( false !== strpos( $file, $i ) ) { 115 | return false; 116 | } 117 | } 118 | 119 | return true; 120 | } ) ); 121 | 122 | printf( 123 | "Found %d files. Parsing hooks...\n", 124 | count( $files ) 125 | ); 126 | 127 | /** 128 | * Fixes newline handling in parsed text. 129 | * 130 | * DocBlock lines, particularly for descriptions, generally adhere to a given character width. For sentences and 131 | * paragraphs that exceed that width, what is intended as a manual soft wrap (via line break) is used to ensure 132 | * on-screen/in-file legibility of that text. These line breaks are retained by phpDocumentor. However, consumers 133 | * of this parsed data may believe the line breaks to be intentional and may display the text as such. 134 | * 135 | * This function fixes text by merging consecutive lines of text into a single line. A special exception is made 136 | * for text appearing in `` and `
` tags, as newlines appearing in those tags are always intentional.
137 |  *
138 |  * @param string $text
139 |  *
140 |  * @return string
141 |  */
142 | function fix_newlines( $text ) {
143 | 	// Non-naturally occurring string to use as temporary replacement.
144 | 	$replacement_string = '{{{{{}}}}}';
145 | 
146 | 	// Replace newline characters within 'code' and 'pre' tags with replacement string.
147 | 	$text = preg_replace_callback(
148 | 		"/(?<=
)(.+)(?=<\/code><\/pre>)/s",
149 | 		function ( $matches ) use ( $replacement_string ) {
150 | 			return preg_replace( '/[\n\r]/', $replacement_string, $matches[1] );
151 | 		},
152 | 		$text
153 | 	);
154 | 
155 | 	// Merge consecutive non-blank lines together by replacing the newlines with a space.
156 | 	$text = preg_replace(
157 | 		"/[\n\r](?!\s*[\n\r])/m",
158 | 		' ',
159 | 		$text
160 | 	);
161 | 
162 | 	// Restore newline characters into code blocks.
163 | 	$text = str_replace( $replacement_string, "\n", $text );
164 | 
165 | 	return $text;
166 | }
167 | 
168 | class DocblockFinderVisitor extends FindingVisitor {
169 | 	private ?Doc $latest_comment = null;
170 | 
171 | 	public function enterNode(Node $node) {
172 | 		$comment = $node->getDocComment();
173 | 
174 | 		if ( $comment ) {
175 | 			$this->latest_comment = $comment;
176 | 		}
177 | 
178 | 		$filterCallback = $this->filterCallback;
179 | 		if ($filterCallback($node)) {
180 | 			if ( $this->latest_comment && $this->latest_comment->getEndLine() + 1 === $node->getStartLine() ) {
181 | 				$node->setDocComment($this->latest_comment);
182 | 			}
183 | 
184 | 			$this->foundNodes[] = $node;
185 | 		}
186 | 
187 | 		return null;
188 | 	}
189 | }
190 | 
191 | /**
192 |  * @param array $files
193 |  * @param string            $root
194 |  * @param array $ignore_hooks
195 |  * @return array
196 |  */
197 | function hooks_parse_files( array $files, string $root, array $ignore_hooks ) : array {
198 | 	$output = array();
199 | 
200 | 	// Create a new parser instance
201 | 	$parser = ( new ParserFactory() )->createForNewestSupportedVersion();
202 | 
203 | 	$funcs = [
204 | 		'do_action',
205 | 		'apply_filters',
206 | 		'do_action_ref_array',
207 | 		'apply_filters_ref_array',
208 | 	];
209 | 
210 | 	foreach ( $files as $filename ) {
211 | 		// Parse the PHP file
212 | 		$contents = file_get_contents($filename);
213 | 
214 | 		if ($contents === false) {
215 | 			throw new \Exception('Failed to read file ' . $filename);
216 | 		}
217 | 
218 | 		$stmts = $parser->parse($contents);
219 | 
220 | 		if (!is_array($stmts)) {
221 | 			throw new \Exception('Failed to parse file ' . $filename);
222 | 		}
223 | 
224 | 		// Create a new FindingVisitor instance
225 | 		$visitor = new DocblockFinderVisitor(
226 | 			fn ( Node $node ) => ( $node instanceof Node\Expr\FuncCall )
227 | 		);
228 | 
229 | 		// Traverse the AST and resolve names
230 | 		$traverser = new NodeTraverser();
231 | 		$traverser->addVisitor( $visitor );
232 | 		$traverser->traverse($stmts);
233 | 
234 | 		/** @var array $found */
235 | 		$found = $visitor->getFoundNodes();
236 | 
237 | 		// Process the parsed statements to find calls to do_action() and apply_filters()
238 | 		foreach ( $found as $expr ) {
239 | 			$funcName = $expr->name;
240 | 
241 | 			if (! ($funcName instanceof Node\Name)) {
242 | 				continue;
243 | 			}
244 | 
245 | 			$funcNameStr = $funcName->toString();
246 | 
247 | 			if ( ! in_array( $funcNameStr, $funcs, true ) ) {
248 | 				continue;
249 | 			}
250 | 
251 | 			$docblock = $expr->getDocComment();
252 | 
253 | 			if ( $docblock && str_starts_with($docblock->getText(), '/** This action is documented in') ) {
254 | 				continue;
255 | 			}
256 | 
257 | 			if ( $docblock && str_starts_with($docblock->getText(), '/** This filter is documented in') ) {
258 | 				continue;
259 | 			}
260 | 
261 | 			$printer = new Standard();
262 | 			$hook_name = $printer->prettyPrintExpr( $expr->args[0]->value );
263 | 			$hook_name = preg_replace( '/^"(.*)"$/', '$1', $hook_name );
264 | 			$hook_name = preg_replace( "/^'(.*)'$/", '$1', $hook_name );
265 | 
266 | 			if ( in_array( $hook_name, $ignore_hooks, true ) ) {
267 | 				continue;
268 | 			}
269 | 
270 | 			$known_problem_hooks = [
271 | 				'autocomplete_users_for_site_admins',
272 | 				'enable_edit_any_user_configuration',
273 | 				'enqueue_block_assets',
274 | 				'show_recent_comments_widget_style',
275 | 			];
276 | 
277 | 			if ( ! ( $docblock instanceof Doc ) ) {
278 | 				echo sprintf(
279 | 					"Hook '%s' in file '%s' is missing a docblock.\n",
280 | 					$hook_name,
281 | 					$filename,
282 | 				);
283 | 
284 | 				continue;
285 | 			}
286 | 
287 | 			$dbt = $docblock ? $docblock->getText() : '';
288 | 
289 | 			if ( empty( $dbt ) ) {
290 | 				if ( in_array( $hook_name, $known_problem_hooks, true ) ) {
291 | 					continue;
292 | 				}
293 | 
294 | 				echo sprintf(
295 | 					"Hook '%s' in file '%s' has an empty docblock.\n",
296 | 					$hook_name,
297 | 					$filename,
298 | 				);
299 | 
300 | 				continue;
301 | 			}
302 | 
303 | 			$doc = [
304 | 				'description' => '',
305 | 				'long_description' => '',
306 | 				'tags' => [],
307 | 				'long_description_html' => '',
308 | 			];
309 | 
310 | 			$dbf = DocBlockFactory::createInstance();
311 | 			$db = $dbf->create( $dbt );
312 | 			$summary = trim( $db->getSummary() );
313 | 			$tags = [];
314 | 
315 | 			foreach ( $db->getTags() as $tag ) {
316 | 				$content = '';
317 | 
318 | 				if ( ! method_exists( $tag, 'getVersion' ) && method_exists( $tag, 'getDescription' ) ) {
319 | 					$content = (string) $tag->getDescription();
320 | 					$content = preg_replace( '#\n\s+#', ' ', $content );
321 | 				}
322 | 
323 | 				$tag_data = [
324 | 					'name' => $tag->getName(),
325 | 					'content' => fix_newlines($content),
326 | 				];
327 | 
328 | 				if ( $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\InvalidTag && $tag->getName() === 'since' ) {
329 | 					$tag_data['content'] = (string) $tag;
330 | 					$tag_data['description'] = (string) $tag;
331 | 				} elseif ( $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Since ) {
332 | 					// Version string.
333 | 					$version = $tag->getVersion();
334 | 
335 | 					if ( ! empty( $version ) ) {
336 | 						$tag_data['content'] = $version;
337 | 					}
338 | 
339 | 					// Description string.
340 | 					$description = preg_replace( '/[\n\r]+/', ' ', strval( $tag->getDescription() ) );
341 | 
342 | 					if ( ! empty( $description ) ) {
343 | 						$markdown = \Parsedown::instance();
344 | 						$html = $markdown->text( $description );
345 | 						$html = preg_replace( '/^

(.*)<\/p>$/', '$1', $html ); 346 | $tag_data['description'] = $html; 347 | } 348 | } elseif ( $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Deprecated ) { 349 | $tag_data['content'] = (string) $tag; 350 | } elseif ( $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Param ) { 351 | $tag_data['types'] = explode( '|', (string) $tag->getType() ); 352 | $tag_data['variable'] = '$' . $tag->getVariableName(); 353 | 354 | $markdown = \Parsedown::instance(); 355 | $html = $markdown->text( $tag_data['content'] ); 356 | $html = preg_replace( '/^

(.*)<\/p>$/', '$1', $html ); 357 | $tag_data['content'] = $html; 358 | } elseif ( $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Link ) { 359 | $link = $tag->getLink(); 360 | $tag_data['content'] = sprintf( 361 | '%s', 362 | $link, 363 | $link 364 | ); 365 | $tag_data['link'] = $link; 366 | } elseif ( $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Generic ) { 367 | // 368 | } elseif ( $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\See ) { 369 | $tag_data['refers'] = ltrim( (string) $tag->getReference(), '\\' ); 370 | $markdown = \Parsedown::instance(); 371 | $html = $markdown->text( $tag_data['content'] ); 372 | $html = preg_replace( '/^

(.*)<\/p>$/', '$1', $html ); 373 | $tag_data['content'] = $html; 374 | } elseif ( $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\InvalidTag && $tag->getName() === 'see' ) { 375 | // 376 | } elseif ( $tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Return_ ) { 377 | printf( 378 | 'Hook "%s" contains a `@return` tag, which is not supported.' . "\n", 379 | $hook_name, 380 | ); 381 | } else { 382 | throw new \Exception( 383 | sprintf( 384 | 'Unknown tag type "%s" (@%s) for hook "%s" in file "%s".', 385 | get_class( $tag ), 386 | $tag->getName(), 387 | $hook_name, 388 | $filename, 389 | ) 390 | ); 391 | } 392 | 393 | $tags[] = $tag_data; 394 | } 395 | 396 | $markdown = \Parsedown::instance(); 397 | $html = $markdown->text((string) $db->getDescription()); 398 | $html = str_replace( "\n", ' ', $html ); 399 | $long = fix_newlines( (string) $db->getDescription() ); 400 | $long = str_replace( 401 | ' - ', 402 | "\n - ", 403 | $long 404 | ); 405 | $long = preg_replace_callback( 406 | '# ([1-9])\. #', 407 | static function( array $matches ) : string { 408 | return "\n {$matches[1]}. "; 409 | }, 410 | $long 411 | ); 412 | $doc = [ 413 | 'description' => str_replace( "\n", ' ', $summary ), 414 | 'long_description' => $long, 415 | 'tags' => $tags, 416 | 'long_description_html' => $html, 417 | ]; 418 | $out = []; 419 | 420 | $out['name'] = $hook_name; 421 | 422 | $aliases = parse_aliases( $html ); 423 | 424 | if ( $aliases ) { 425 | $out['aliases'] = $aliases; 426 | } 427 | 428 | $out['file'] = str_replace( "{$root}/", '', $filename ); 429 | 430 | switch ( $funcNameStr ) { 431 | case 'do_action': 432 | $out['type'] = 'action'; 433 | break; 434 | case 'apply_filters': 435 | $out['type'] = 'filter'; 436 | break; 437 | case 'do_action_ref_array': 438 | $out['type'] = 'action_reference'; 439 | break; 440 | case 'apply_filters_ref_array': 441 | $out['type'] = 'filter_reference'; 442 | break; 443 | } 444 | 445 | $out['doc'] = $doc; 446 | $out['args'] = count( $expr->args ) - 1; 447 | 448 | $output[] = $out; 449 | } 450 | } 451 | 452 | usort( $output, function( array $a, array $b ) : int { 453 | return strcmp( $a['name'], $b['name'] ); 454 | } ); 455 | 456 | return $output; 457 | } 458 | 459 | /** 460 | * @return array 461 | */ 462 | function parse_aliases( string $html ) : array { 463 | if ( false === strpos( $html, 'Possible hook names include' ) ) { 464 | return []; 465 | } 466 | 467 | $aliases = []; 468 | 469 | $html = explode( 'Possible hook names include', $html, 2 ); 470 | $html = explode( '', end( $html ) ); 471 | 472 | $dom = new DOMDocument(); 473 | $dom->loadHTML( reset( $html ) ); 474 | 475 | foreach ( $dom->getElementsByTagName( 'li' ) as $li ) { 476 | $aliases[] = $li->nodeValue; 477 | } 478 | 479 | sort( $aliases ); 480 | 481 | return $aliases; 482 | } 483 | 484 | $output = hooks_parse_files( $files, $source_dir, $ignore_hooks ); 485 | 486 | // Actions 487 | $actions = array_values( array_filter( $output, function( array $hook ) : bool { 488 | return in_array( $hook['type'], [ 'action', 'action_reference' ], true ); 489 | } ) ); 490 | 491 | $actions = [ 492 | '$schema' => 'https://raw.githubusercontent.com/wp-hooks/generator/1.0.1/schema.json', 493 | 'hooks' => $actions, 494 | ]; 495 | 496 | $result = file_put_contents( $target_dir . '/actions.json', json_encode( $actions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); 497 | 498 | // Filters 499 | $filters = array_values( array_filter( $output, function( array $hook ) : bool { 500 | return in_array( $hook['type'], [ 'filter', 'filter_reference' ], true ); 501 | } ) ); 502 | 503 | $filters = [ 504 | '$schema' => 'https://raw.githubusercontent.com/wp-hooks/generator/1.0.1/schema.json', 505 | 'hooks' => $filters, 506 | ]; 507 | 508 | $result = file_put_contents( $target_dir . '/filters.json', json_encode( $filters, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); 509 | 510 | echo "Done\n"; 511 | -------------------------------------------------------------------------------- /src/validate.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | resolver()->registerFile( 17 | $id, 18 | 'schema.json', 19 | ); 20 | 21 | $result = $validator->validate( $data, $id ); 22 | 23 | if ($result->isValid()) { 24 | echo 'Data is valid', PHP_EOL; 25 | } else { 26 | print_r( ( new ErrorFormatter() )->format($result->error() ) ); 27 | exit(1); 28 | } 29 | --------------------------------------------------------------------------------