├── wp-content ├── uploads │ ├── 2024 │ │ └── 04 │ │ │ ├── L-Misc2.jpg │ │ │ ├── 22_import-button.png │ │ │ ├── 23_export-button.png │ │ │ ├── 21_wp-version-switcher.png │ │ │ ├── 21_wp-version-switcher-1536x1171.png │ │ │ ├── 21_wp-version-switcher-2048x1562.png │ │ │ ├── CleanShot-2024-04-02-at-11.57.25@2x.png │ │ │ ├── 21_wp-version-switcher-2048x1562-1536x1172.png │ │ │ ├── 37_c-programs-php-4a55d44329eb4e6403983664c6492f5b.png │ │ │ ├── 36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d.png │ │ │ ├── 37_c-programs-php-4a55d44329eb4e6403983664c6492f5b-1536x993.png │ │ │ ├── 37_c-programs-php-4a55d44329eb4e6403983664c6492f5b-2048x1324.png │ │ │ ├── 38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b.png │ │ │ ├── 36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d-1536x993.png │ │ │ ├── 36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d-2048x1324.png │ │ │ ├── 38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b-1536x993.png │ │ │ ├── 37_c-programs-php-4a55d44329eb4e6403983664c6492f5b-2048x1324-1536x993.png │ │ │ ├── 38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b-2048x1324.png │ │ │ ├── 36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d-2048x1324-1536x993.png │ │ │ └── 38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b-2048x1324-1536x993.png │ └── .gitkeep ├── html-pages │ ├── 1_developer-apis │ │ ├── index.html │ │ └── 1_query-api.html │ ├── 2_architecture │ │ ├── index.html │ │ └── 4_webassembly-php │ │ │ ├── 2_php-filesystem.html │ │ │ ├── index.html │ │ │ ├── 3_asyncify.html │ │ │ └── 1_compiling-php.html │ └── 0_wordpress-playground │ │ ├── index.html │ │ ├── 0_overview.html │ │ └── 1_start-using-wordpress-playground-in-5-minutes.html ├── themes │ └── playground-docs │ │ ├── parts │ │ ├── footer.html │ │ └── header.html │ │ ├── theme.json │ │ ├── screenshot.png │ │ ├── style.css │ │ ├── readme.txt │ │ ├── templates │ │ ├── page.html │ │ └── home.html │ │ └── patterns │ │ └── footer.php └── plugins │ ├── export-static-site │ └── export-static-site.php │ └── wp-docs-plugin │ ├── playground-post-export-processor.php │ └── plugin.php ├── .gitignore ├── .prettierrc ├── start-server.sh ├── blueprint-browser.json ├── .github └── workflows │ └── gh-pages.yml ├── blueprint-serve.json ├── blueprint-static-site.json └── README.md /wp-content/uploads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | wp-content/uploads/simply-static -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 4 4 | } 5 | -------------------------------------------------------------------------------- /wp-content/html-pages/1_developer-apis/index.html: -------------------------------------------------------------------------------- 1 |

Developer APIs

2 | 3 | -------------------------------------------------------------------------------- /wp-content/themes/playground-docs/parts/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wp-content/themes/playground-docs/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "$schema": "https://schemas.wp.org/wp/6.5/theme.json" 4 | } -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/L-Misc2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/L-Misc2.jpg -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/22_import-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/22_import-button.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/23_export-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/23_export-button.png -------------------------------------------------------------------------------- /wp-content/themes/playground-docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/themes/playground-docs/screenshot.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/21_wp-version-switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/21_wp-version-switcher.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/21_wp-version-switcher-1536x1171.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/21_wp-version-switcher-1536x1171.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/21_wp-version-switcher-2048x1562.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/21_wp-version-switcher-2048x1562.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/CleanShot-2024-04-02-at-11.57.25@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/CleanShot-2024-04-02-at-11.57.25@2x.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/21_wp-version-switcher-2048x1562-1536x1172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/21_wp-version-switcher-2048x1562-1536x1172.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/37_c-programs-php-4a55d44329eb4e6403983664c6492f5b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/37_c-programs-php-4a55d44329eb4e6403983664c6492f5b.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/37_c-programs-php-4a55d44329eb4e6403983664c6492f5b-1536x993.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/37_c-programs-php-4a55d44329eb4e6403983664c6492f5b-1536x993.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/37_c-programs-php-4a55d44329eb4e6403983664c6492f5b-2048x1324.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/37_c-programs-php-4a55d44329eb4e6403983664c6492f5b-2048x1324.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d-1536x993.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d-1536x993.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d-2048x1324.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d-2048x1324.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b-1536x993.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b-1536x993.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/37_c-programs-php-4a55d44329eb4e6403983664c6492f5b-2048x1324-1536x993.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/37_c-programs-php-4a55d44329eb4e6403983664c6492f5b-2048x1324-1536x993.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b-2048x1324.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b-2048x1324.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d-2048x1324-1536x993.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/36_c-programs-general-dc8c885b6c55e554f0c504f32e49ad8d-2048x1324-1536x993.png -------------------------------------------------------------------------------- /wp-content/uploads/2024/04/38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b-2048x1324-1536x993.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamziel/playground-docs-workflow/HEAD/wp-content/uploads/2024/04/38_c-programs-php-versions-d204a7325079ab708fd605a6a1d2681b-2048x1324-1536x993.png -------------------------------------------------------------------------------- /start-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p output 4 | bunx @wp-playground/cli@latest \ 5 | server \ 6 | --mount=./wp-content/plugins/wp-docs-plugin:/wordpress/wp-content/plugins/wp-docs-plugin \ 7 | --mount=./wp-content/plugins/export-static-site:/wordpress/wp-content/plugins/export-static-site \ 8 | --mount=./wp-content/html-pages:/wordpress/wp-content/html-pages \ 9 | --mount=./wp-content/uploads:/wordpress/wp-content/uploads \ 10 | --mount=./wp-content/themes/playground-docs:/wordpress/wp-content/themes/playground-docs \ 11 | --mount=./output:/output \ 12 | --blueprint=./blueprint-serve.json \ 13 | --wp=6.5 \ 14 | --php=8.0 15 | -------------------------------------------------------------------------------- /blueprint-browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json", 3 | "landingPage": "/wp-admin/edit.php?post_type=page", 4 | "preferredVersions": { 5 | "wp": "6.5", 6 | "php": "8.0" 7 | }, 8 | "login": true, 9 | "steps": [ 10 | { 11 | "step": "unzip", 12 | "zipFile": { 13 | "resource": "url", 14 | "url": "https://github-proxy.com/partial/adamziel/playground-docs-workflow/wp-content" 15 | }, 16 | "extractToPath": "/wordpress" 17 | }, 18 | { 19 | "step": "installPlugin", 20 | "pluginZipFile": { 21 | "resource": "wordpress.org/plugins", 22 | "slug": "create-block-theme" 23 | } 24 | }, 25 | { 26 | "step": "installPlugin", 27 | "pluginZipFile": { 28 | "resource": "wordpress.org/plugins", 29 | "slug": "gutenberg" 30 | } 31 | }, 32 | { 33 | "step": "activatePlugin", 34 | "pluginPath": "wp-docs-plugin/plugin.php" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /wp-content/plugins/export-static-site/export-static-site.php: -------------------------------------------------------------------------------- 1 | run_static_export(); 15 | 16 | // Wait at most 2 minutes for the export to finish 17 | $i = 0; 18 | do { 19 | $exports = glob(WP_CONTENT_DIR . '/uploads/simply-static/temp-files/*.zip'); 20 | sleep(1); 21 | } while (empty($exports) && ++$i < 120); 22 | 23 | if (empty($exports)) { 24 | throw new Exception("The export wasn't finished in two minutes, aborting."); 25 | } 26 | 27 | $export = end($exports); 28 | rename($export, $output_file); 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Generate Static Site 2 | 3 | on: 4 | push: 5 | branches: 6 | - trunk 7 | workflow_dispatch: 8 | 9 | 10 | jobs: 11 | # Build job 12 | build: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write # to deploy to Pages 16 | timeout-minutes: 5 17 | env: 18 | REPO: ${{ github.repository }} 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | - name: 'Install bun' 23 | run: | 24 | curl -fsSL https://bun.sh/install | bash 25 | - name: Generate static site 26 | run: PATH="${PATH}:${HOME}/.bun/bin" bash build-static-site.sh 27 | - name: Deploy 28 | uses: peaceiris/actions-gh-pages@v4 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./output 32 | -------------------------------------------------------------------------------- /wp-content/themes/playground-docs/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Theme Name: Playground Docs 3 | Theme URI: https://wordpress.org/themes/twentytwentyfour/ 4 | Author: the WordPress team 5 | Author URI: https://wordpress.org 6 | Description: Twenty Twenty-Four is designed to be flexible, versatile and applicable to any website. Its collection of templates and patterns tailor to different needs, such as presenting a business, blogging and writing or showcasing work. A multitude of possibilities open up with just a few adjustments to color and typography. Twenty Twenty-Four comes with style variations and full page designs to help speed up the site building process, is fully compatible with the site editor, and takes advantage of new design tools introduced in WordPress 6.4. 7 | Requires at least: 6.0 8 | Tested up to: 6.5.2 9 | Requires PHP: 5.7 10 | Version: 1.0.0 11 | License: GNU General Public License v2 or later 12 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 13 | Template: twentytwentyfour 14 | Text Domain: playground-docs 15 | Tags: 16 | */ 17 | 18 | -------------------------------------------------------------------------------- /wp-content/html-pages/2_architecture/index.html: -------------------------------------------------------------------------------- 1 |

Architecture

2 | 3 | 4 |

WordPress Playground consists of the following high-level components:

5 | 6 | 7 | 8 | 27 | 28 | 29 | 30 |

Visit each section to learn more about the specific parts of the architecture.

31 | -------------------------------------------------------------------------------- /wp-content/themes/playground-docs/parts/header.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /blueprint-serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json", 3 | "landingPage": "/wp-admin/edit.php?post_type=page", 4 | "preferredVersions": { 5 | "wp": "6.5", 6 | "php": "8.0" 7 | }, 8 | "login": true, 9 | "steps": [ 10 | { 11 | "step": "installTheme", 12 | "themeZipFile": { 13 | "resource": "wordpress.org/themes", 14 | "slug": "twentytwentyfour" 15 | } 16 | }, 17 | { 18 | "step": "mkdir", 19 | "path": "/wordpress/wp-content/mu-plugins" 20 | }, 21 | { 22 | "step": "writeFile", 23 | "path": "/wordpress/wp-content/mu-plugins/0-hide-admin-bar.php", 24 | "data": "PHP Filesystem 2 | 3 | 4 |

The PHP module has its own filesystem separate from your computer's filesystem. It is provided by Emscripten's FS library and the default APIs is low-level and cumbersome to use. The PHP JavaScript class shipped with WordPress Playground wraps it with a more convenient higher-level API.

5 | 6 | 7 | 8 |

In general, WordPress Playground uses an in-memory virtual filesystem.

9 | 10 | 11 | 12 |

However, in Node.js, you can also mount a real directory from the host filesystem into the PHP filesystem.

13 | 14 | 15 | 16 |

Here's how to interact with the filesystem in WordPress Playground:

17 | 18 | 19 | 20 |
// Recursively create a /var/www directory
21 | php.mkdirTree('/var/www');
22 | 
23 | console.log(php.fileExists('/var/www/file.txt'));
24 | // false
25 | 
26 | php.writeFile('/var/www/file.txt', 'Hello from the filesystem!');
27 | 
28 | console.log(php.fileExists('/var/www/file.txt'));
29 | // true
30 | 
31 | console.log(php.readFile('/var/www/file.txt'));
32 | // "Hello from the filesystem!
33 | 
34 | // Delete the file:
35 | php.unlink('/var/www/file.txt');
36 | 37 | 38 | 39 |

For more details consult the BasePHP class directly – it has some great documentation strings.

40 | -------------------------------------------------------------------------------- /blueprint-static-site.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json", 3 | "landingPage": "/wp-admin/edit.php?post_type=page", 4 | "preferredVersions": { 5 | "wp": "6.5", 6 | "php": "8.0" 7 | }, 8 | "login": true, 9 | "steps": [ 10 | { 11 | "step": "defineWpConfigConsts", 12 | "consts": { 13 | "SIMPLY_STATIC_DESTINATION_URL_TYPE": "relative", 14 | "SIMPLY_STATIC_RELATIVE_PATH": "/playground-docs-workflow" 15 | } 16 | }, 17 | { 18 | "step": "installTheme", 19 | "themeZipFile": { 20 | "resource": "wordpress.org/themes", 21 | "slug": "twentytwentyfour" 22 | } 23 | }, 24 | { 25 | "step": "mkdir", 26 | "path": "/wordpress/wp-content/mu-plugins" 27 | }, 28 | { 29 | "step": "writeFile", 30 | "path": "/wordpress/wp-content/mu-plugins/0-hide-admin-bar.php", 31 | "data": " 2 | 3 | 4 |
5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /wp-content/themes/playground-docs/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## WordPress Playground for Documenters 2 | 3 | This is an attempt to create a buildless Documentation Contributor Workflow that uses WordPress Playground to: 4 | 5 | * Fetch the latest version of the documentation from the repository. 6 | * Edit documentation in a browser-based editor. 7 | * Preview the changes in real-time. 8 | * Submit the changes as a pull request. 9 | * Provide a live preview of the documentation PR. 10 | 11 | [
Browse the Documentation
](https://adamziel.github.io/playground-docs-workflow/). 12 | 13 | ## How to edit the documentation? 14 | 15 | ### In WordPress Playground 16 | 17 | Click here to try it: 18 | 19 | [
Edit the Documentation
](https://playground.wordpress.net/?gh-ensure-auth=yes&ghexport-repo-url=https%3A%2F%2Fgithub.com%2Fadamziel%2Fplayground-docs-workflow&ghexport-content-type=custom-paths&ghexport-path=plugins/wp-docs-plugin&ghexport-path=plugins/export-static-site&ghexport-path=themes/playground-docs&ghexport-path=html-pages&ghexport-path=uploads&ghexport-commit-message=Documentation+update&ghexport-playground-root=/wordpress/wp-content&ghexport-repo-root=/wp-content&blueprint-url=https%3A%2F%2Fraw.githubusercontent.com%2Fadamziel%2Fplayground-docs-workflow%2Ftrunk%2Fblueprint-browser.json&ghexport-pr-action=create&ghexport-allow-include-zip=no). 20 | 21 | It should load the doc pages from the `html-pages` directory and the media attachments from `uploads`. This video demonstrates it: 22 | 23 | https://github.com/adamziel/playground-docs-workflow/assets/205419/5d06d8b8-cd9f-4cec-a8c6-e73d66e82159 24 | 25 | ### Locally 26 | 27 | To start a local server with the documentation site, run: 28 | 29 | ```bash 30 | bash start-server.sh 31 | ``` 32 | 33 | You'll need node.js and npm installed. 34 | 35 | Once you're done editing the documentation, commit your changes as follows: 36 | 37 | ```bash 38 | git add wp-content 39 | git commit -a 40 | ``` 41 | 42 | And then submit a Pull Request to the repository. 43 | 44 | ## How to edit the site theme? 45 | 46 | Adjust the site as needed in the site editor and then use the preinstalled [create-block-theme](https://github.com/WordPress/create-block-theme/) plugin to [save the theme updates](https://github.com/WordPress/create-block-theme/?tab=readme-ov-file#how-to-use-the-plugin) and propose them as a PR. 47 | 48 | ## Remaining work 49 | 50 | - [ ] Put preview links in the GitHub PRs 51 | -------------------------------------------------------------------------------- /wp-content/themes/playground-docs/patterns/footer.php: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 |
23 |
24 |
25 |

26 | 27 | 28 | 29 |
30 |
    31 |
  • Documentation', 'twentytwentyfour');?>
  • 32 | 33 | 34 | 35 |
  • API Reference', 'twentytwentyfour');?>
  • 36 |
37 |
38 |
39 | 40 | 41 | 42 |
43 |

44 | 45 | 46 | 47 |
48 |
    49 |
  • GitHub', 'twentytwentyfour');?>
  • 50 | 51 | 52 | 53 |
  • #meta-playground on Slack', 'twentytwentyfour');?>
  • 54 |
55 |
56 |
57 |
58 |
59 |
60 | 61 | 62 | 63 |

64 | 65 | 66 | 67 |
68 | 69 |
70 |
71 | -------------------------------------------------------------------------------- /wp-content/html-pages/2_architecture/4_webassembly-php/index.html: -------------------------------------------------------------------------------- 1 |

WebAssembly PHP

2 | 3 | 4 |

WordPress Playground build the PHP interpreter to WebAssembly using Emscripten and a dedicated pipeline.

5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 |

Building PHP to WebAssembly is very similar to building vanilla PHP. The wasm build required adjusting a function signature here, forcing a config variable there, and applying a few small patches, but there's relatively few adjustments involved.

13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |

However, vanilla PHP builds aren't very useful in the browser. As a server software, PHP doesn't have a JavaScript API to pass the request body, upload files, or populate the php://stdin stream. WordPress Playground had to build one from scratch. The WebAssembly binary comes with a dedicated PHP API module written in C and a JavaScript PHP class that exposes methods like writeFile() or run().

21 | 22 | 23 | 24 |

Because every PHP version is just a static .wasm file, the PHP version switcher is actually pretty boring. It simply tells the browser to download, for example, php_7_3.wasm instead of, say, php_8_2.wasm.

25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 |

Networking support varies between platforms

33 | 34 | 35 | 36 |

When it comes to networking, WebAssembly programs are limited to calling JavaScript APIs. It is a safety feature, but also presents a challenge. How do you support low-level, synchronous networking code used by PHP with the high-level asynchronous APIs available in JavaScript?

37 | 38 | 39 | 40 |

In Node.js, the answer involves a WebSocket to TCP socket proxy, Asyncify, and patching deep PHP internals like php_select. It's complex, but there's a reward. The Node.js-targeted PHP build can request web APIs, install composer packages, and even connect to a MySQL server.

41 | 42 | 43 | 44 |

In the browser, networking is supported to a limited extent. Network calls initiated using wp_safe_remote_get, like the ones in the plugin directory or the font library, are translated into fetch() calls and succeed if the remote server sends the correct CORS headers. However, a full support for arbitrary HTTPS connection involves opening a raw TCP socket which is not possible in the browser. There is an open GitHub issue that explores possible ways of addressing this problem.

45 | -------------------------------------------------------------------------------- /wp-content/html-pages/2_architecture/4_webassembly-php/3_asyncify.html: -------------------------------------------------------------------------------- 1 |

Asyncify

2 | 3 | 4 |

Asyncify lets synchronous C or C++ code interact with asynchronous JavaScript. Technically, it saves the entire C call stack before yielding control back to JavaScript, and then restores it when the asynchronous call is finished. This is called stack switching.

5 | 6 | 7 | 8 |

Networking support in the WebAssembly PHP build is implemented using Asyncify. When PHP makes a network request, it yields control back to JavaScript, which makes the request, and then resumes PHP when the response is ready. It works well enough that PHP build can request web APIs, install composer packages, and even connect to a MySQL server.

9 | 10 | 11 | 12 |

Asyncify crashes

13 | 14 | 15 | 16 |

Stack switching requires wrapping all C functions that may be found at a call stack at a time of making an asynchronous call. Blanket-wrapping of every single C function adds a significant overhead, which is why we maintain a list of specific function names:

17 | 18 | 19 | 20 |

https://github.com/WordPress/wordpress-playground/blob/15a660940ee9b4a332965ba2a987f6fda0c159b1/packages/php-wasm/compile/Dockerfile#L624-L632

21 | 22 | 23 | 24 |

Unfortunately, missing even a single item from that list results in a WebAssembly crash whenever that function is a part of the call stack when an asynchronous call is made. It looks like this:

25 | 26 | 27 | 28 |
A screenshot of an asyncify error in the terminal
29 | 30 | 31 | 32 |

Asyncify can auto-list all the required C functions when built without ASYNCIFY_ONLY, but that auto-detection is overeager and ends up listing about 70,000 C functions which increases the startup time to 4.5s. That's why we maintain the list manually.

33 | 34 | 35 | 36 |

If you are interested in more details, see GitHub issue 251.

37 | 38 | 39 | 40 |

Fixing Asyncify crashes

41 | 42 | 43 | 44 |

Pull Request 253 adds a fix-asyncify command that runs a specialized test suite and automatically adds any identified missing C functions to the ASYNCIFY_ONLY list.

45 | 46 | 47 | 48 |

If you run into a crash like the one above, you can fix it by:

49 | 50 | 51 | 52 |
    53 |
  1. Identifying a PHP code path that triggers the crash – the stack trace in the terminal should help with that.
  2. 54 | 55 | 56 | 57 |
  3. Adding a test case that triggers a crash to packages/php-wasm/node/src/test/php-asyncify.spec.ts
  4. 58 | 59 | 60 | 61 |
  5. Running: npm run fix-asyncify
  6. 62 | 63 | 64 | 65 |
  7. Committing the test case, the updated Dockerfile, and the rebuilt PHP.wasm
  8. 66 |
67 | 68 | 69 | 70 |

The upcoming JSPI API will make Asyncify unnecessary

71 | 72 | 73 | 74 |

Eventually, V8 will likely handle stack switching for us and remove this problem entirely. Issue 134 tracks the status of that effort.

75 | 76 | 77 | 78 |

Here's a relevant note from @fgmccabe:

79 | 80 | 81 | 82 |
83 |

The current implementation in V8 is essentially 'experimental status'. We have arm64 and x64 implementations.
The next steps are to implement on 32 bit arm/intel. That requires us to solve some issues that we did not have to solve so far.
As for node.js, my guess is that it is already in node, behind a flag.
To remove the flag requirement involves getting other implementations. The best estimate for that is towards the end of this year; but it obviously depends on resources and funding.
In addition, it would need further progress in the standardization effort; but, given that it is a 'small' spec, that should not be a long term burden.
Hope that this helps you understand the roadmap :)

84 |
85 | -------------------------------------------------------------------------------- /wp-content/html-pages/0_wordpress-playground/index.html: -------------------------------------------------------------------------------- 1 |

WordPress Playground

2 | 3 | 4 |
5 |

Looking for the official Playground website?

6 | 7 | 8 | 9 |

WordPress Playground website moved to wordpress.org/playground. The site you're at now hosts the documentation.

10 | 11 | 12 | 13 |

14 |
15 | 16 | 17 | 18 |

👋 Hi! Welcome to WordPress Playground documentation. Playground is an online tool to experiment and learn about WordPress – learn more in the [overview section](./02-overview.md).

19 | 20 | 21 | 22 |

The documentation consists of two major sections:

23 | 24 | 25 | 26 |

- [Documentation](./01-index.md) (you're here) – Introduction, concepts, and guides

27 | 28 | 29 | 30 |

- [API reference](/api) – All the APIs exposed by WordPress Playground

31 | 32 | 33 | 34 |

This site (Documentation) is where you will find all the information you need to start using Playground. To learn more about what this fantastic tool, read [Introduction to Playground: running WordPress in the browser](https://developer.wordpress.org/news/2024/04/05/introduction-to-playground-running-wordpress-in-the-browser/)

35 | 36 | 37 | 38 |

Quick start

39 | 40 | 41 | 42 | 61 | 62 | 63 | 64 |

Take a deep dive

65 | 66 | 67 | 68 | 83 | 84 | 85 | 86 |

Get Involved

87 | 88 | 89 | 90 |

WordPress Playground is an open-source project and welcomes all contributors from code to design, and from documentation to triage. Don't worry, you don't need to know WebAssembly to contribute!

91 | 92 | 93 | 94 | 101 | 102 | 103 | 104 |

As with all WordPress projects, we want to ensure a welcoming environment for everyone. With that in mind, all contributors are expected to follow our Code of Conduct.

105 | 106 | 107 | 108 |

License

109 | 110 | 111 | 112 |

WordPress Playground is free software, and is released under the terms of the GNU General Public License version 2 or (at your option) any later version. See LICENSE.md. for complete license.

113 | 114 | 115 | 116 |

117 | -------------------------------------------------------------------------------- /wp-content/html-pages/0_wordpress-playground/0_overview.html: -------------------------------------------------------------------------------- 1 |

Overview

2 | 3 | 4 |

WordPress Playground is an online platform that allows you to experiment and learn about WordPress without affecting your live website. It's a virtual sandbox where you can play around with different features, designs, and settings in a safe and controlled environment.

5 | 6 | 7 | 8 |

Here's how it works

9 | 10 | 11 | 12 |

When you first start using WordPress Playground, you'll be provided with a separate space where you can create and customise your own WordPress website. This space is completely isolated from your actual website.

13 | 14 | 15 | 16 |

Try themes and plugins on the fly

17 | 18 | 19 | 20 |

Within the WordPress Playground, you can explore various themes. You can choose from a wide range of themes and see how they look on your site. You can also modify the colors, fonts, layouts, and other visual elements to create a unique design.
In addition to themes, you can experiment with plugins too. With WordPress Playground, you can install and test different plugins to see how they work and what they can do for your site. This allows you to explore and understand the capabilities of WordPress without worrying about breaking anything.

21 | 22 | 23 | 24 |

Create content on the go

25 | 26 | 27 | 28 |

Another great feature of WordPress Playground is the ability to create and edit content. You can write blog posts, create pages and add media like images and videos to your site. This helps you understand how to organize and structure your content effectively.

29 | 30 | 31 | 32 |

The content you create is limited to the Playground on your device and disappears once you leave it, so you are free to explore and play without risking breaking any actual site.

33 | 34 | 35 | 36 |

And, yes it's safe

37 | 38 | 39 | 40 |

Overall, WordPress Playground provides a risk-free environment for beginners to learn and get hands-on experience with WordPress. It helps you to gain confidence and knowledge before making changes to your live website.

41 | 42 | 43 | 44 |

What makes Playground different from running WordPress on a web server or local desktop app?

45 | 46 | 47 | 48 |

Web applications like WordPress have long-relied on server technologies to run logic and to store data.

49 | 50 | 51 | 52 |

Using those technologies has meant either running an web server connected to the internet or using those technologies in a desktop service or app (sometimes called a "WordPress local environment") that either leans on a virtual server with the technologies installed or the underlying technologies on the current device.

53 | 54 | 55 | 56 |

Playground is a novel way to stream server technologies -- and WordPress (and WP-CLI) -- as files that can then run in the browser.

57 | 58 | 59 | 60 |

Streamed, not served.

61 | 62 | 63 | 64 |

The WordPress you see when you open Playground in your browser is a WordPress that should function like any WordPress, with a few limitations and the important exception that it's not a permanent server with an internet address which will limit connections to some third-party services (automation, sharing, analysis, email, backups, etc.) in a persistient way.

65 | 66 | 67 | 68 |

The loading screen and progress bar you see on Playground includes both the streaming of those foundational technologies to your browser and configuration steps (examples) from WordPress Blueprints, so that a full server, WordPress software, Theme & Plugin solutions and configuration instructions can be streamed over-the-wire.

69 | 70 | 71 | 72 |

While many WordPress solutions may require internet connectivity to interact with social networks, live feeds and other internet services, those kind of connections could be limited in Playground. However, by enabling network connectivity in the Customize Playground settings modal (example URL w/ query parameter), you can mostly wire-up internet connectivity to the WordPress in Playground.

73 | -------------------------------------------------------------------------------- /wp-content/html-pages/2_architecture/4_webassembly-php/1_compiling-php.html: -------------------------------------------------------------------------------- 1 |

Compiling PHP

2 | 3 | 4 |

The build pipeline lives in a Dockerfile. In broad strokes, it:

5 | 6 | 7 | 8 | 35 | 36 | 37 | 38 |

To find out more about each step, refer directly to the Dockerfile.

39 | 40 | 41 | 42 |

Building

43 | 44 | 45 | 46 |

To build all PHP versions, run npm run recompile:php:web (or php-wasm-node) in the repository root. You'll find the output files in packages/php-wasm/php-web/public. To build a specific version, run npm run recompile:php:web:kitchen-sink:8.0 or npm run recompile:php:web:light:8.0 – depending on the build pack.

47 | 48 | 49 | 50 |

The build produces two files: php.wasm and php.js.

51 | 52 | 53 | 54 |

PHP.wasm WebAssembly module

55 | 56 | 57 | 58 |

PHP extensions

59 | 60 | 61 | 62 |

PHP is built with several extensions listed in the Dockerfile.

63 | 64 | 65 | 66 |

Some extensions, like zip, can be turned on or off during the build. Others, like sqlite3, are hardcoded.

67 | 68 | 69 | 70 |

If you need to turn off one of the hardcoded extensions, feel free to open an issue in this repo. Better yet, this project needs contributors. You are more than welcome to open a PR and author the change you need.

71 | 72 | 73 | 74 |

C API exposed to JavaScript

75 | 76 | 77 | 78 |

The C API exposed to JavaScript lives in the php_wasm.c file.

79 | 80 | 81 | 82 |

Refer to the source code and the inline documentation in php_wasm.c to learn more.

83 | 84 | 85 | 86 |

Build configuration

87 | 88 | 89 | 90 |

The build is configurable via the Docker --build-arg feature. You can set them up through the build.js script, just run this command to get the usage message:

91 | 92 | 93 | 94 |
npm run recompile:php:web
95 | 96 | 97 | 98 |

PHP.js JavaScript module

99 | 100 | 101 | 102 |

The php.js file generated by the WebAssembly PHP build pipeline is not a vanilla Emscripten module. Instead, it's an ESM module that wraps the regular Emscripten output and adds some extra functionality.

103 | 104 | 105 | 106 |

Here's the API it exposes:

107 | 108 | 109 | 110 |
// php.wasm size in bytes:
111 | export const dependenciesTotalSize = 5644199;
112 | 
113 | // php.wasm filename:
114 | export const dependencyFilename = 'php.wasm';
115 | 
116 | // Run Emscripten's generated module:
117 | export default function (jsEnv, emscriptenModuleArgs) {}
118 | 119 | 120 | 121 |

The generated JavaScript module is not meant for direct use. Instead, it can be consumed through a NodePHP class in Node.js and a WebPHP class in the browser:

122 | 123 | 124 | 125 |
// In Node.js:
126 | const php = NodePHP.load('7.4');
127 | 
128 | // On the web:
129 | const php = await WebPHP.load('8.0');
130 | 131 | 132 | 133 |

Both of these classes extend the BasePHP class exposed by the @php-wasm/universal package and implement the UniversalPHP interface that standardizes the API across all PHP environments.

134 | 135 | 136 | 137 |

Loading the PHP runtime

138 | 139 | 140 | 141 |

The load() method handles the entire PHP initialization pipeline. In particular, it:

142 | 143 | 144 | 145 | 160 | -------------------------------------------------------------------------------- /wp-content/plugins/wp-docs-plugin/playground-post-export-processor.php: -------------------------------------------------------------------------------- 1 | get_after_opener_tag_and_before_closer_tag_positions(); 53 | if ( ! $positions ) { 54 | return null; 55 | } 56 | 57 | return substr( $this->html, $positions['after_opener_tag'], $positions['before_closer_tag'] - $positions['after_opener_tag'] ); 58 | } 59 | 60 | public function remove_balanced_tag() 61 | { 62 | $positions = $this->get_after_opener_tag_and_before_closer_tag_positions(); 63 | if ( ! $positions ) { 64 | return null; 65 | } 66 | $this->lexical_updates[] = new WP_HTML_Text_Replacement( 67 | $positions['before_opener_tag'], 68 | $positions['after_closer_tag'], 69 | '' 70 | ); 71 | 72 | return true; 73 | 74 | } 75 | 76 | /** 77 | * Sets the content between two balanced tags. 78 | * 79 | * @since 6.5.0 80 | * 81 | * @access private 82 | * 83 | * @param string $new_content The string to replace the content between the matching tags. 84 | * @return bool Whether the content was successfully replaced. 85 | */ 86 | public function set_content_between_balanced_tags( string $new_content ): bool { 87 | $positions = $this->get_after_opener_tag_and_before_closer_tag_positions( true ); 88 | if ( ! $positions ) { 89 | return false; 90 | } 91 | list( $after_opener_tag, $before_closer_tag ) = $positions; 92 | 93 | $this->lexical_updates[] = new WP_HTML_Text_Replacement( 94 | $after_opener_tag, 95 | $before_closer_tag - $after_opener_tag, 96 | esc_html( $new_content ) 97 | ); 98 | 99 | return true; 100 | } 101 | 102 | /** 103 | * Gets the positions right after the opener tag and right before the closer 104 | * tag in a balanced tag. 105 | * 106 | * By default, it positions the cursor in the closer tag of the balanced tag. 107 | * If $rewind is true, it seeks back to the opener tag. 108 | * 109 | * @since 6.5.0 110 | * 111 | * @access private 112 | * 113 | * @param bool $rewind Optional. Whether to seek back to the opener tag after finding the positions. Defaults to false. 114 | * @return array|null Start and end byte position, or null when no balanced tag bookmarks. 115 | */ 116 | private function get_after_opener_tag_and_before_closer_tag_positions( bool $rewind = false ) { 117 | // Flushes any changes. 118 | $this->get_updated_html(); 119 | 120 | $bookmarks = $this->get_balanced_tag_bookmarks(); 121 | if ( ! $bookmarks ) { 122 | return null; 123 | } 124 | list( $opener_tag, $closer_tag ) = $bookmarks; 125 | 126 | $positions = array( 127 | 'before_opener_tag' => $this->bookmarks[$opener_tag]->start, 128 | 'after_opener_tag' => $this->bookmarks[$opener_tag]->start + $this->bookmarks[$opener_tag]->length + 1, 129 | 'before_closer_tag' => $this->bookmarks[$closer_tag]->start, 130 | 'after_closer_tag' => $this->bookmarks[$closer_tag]->start + $this->bookmarks[$closer_tag]->length + 1, 131 | ); 132 | 133 | if ( $rewind ) { 134 | $this->seek( $opener_tag ); 135 | } 136 | 137 | $this->release_bookmark( $opener_tag ); 138 | $this->release_bookmark( $closer_tag ); 139 | 140 | return $positions; 141 | } 142 | 143 | /** 144 | * Returns a pair of bookmarks for the current opener tag and the matching 145 | * closer tag. 146 | * 147 | * It positions the cursor in the closer tag of the balanced tag, if it 148 | * exists. 149 | * 150 | * @since 6.5.0 151 | * 152 | * @return array|null A pair of bookmarks, or null if there's no matching closing tag. 153 | */ 154 | private function get_balanced_tag_bookmarks() { 155 | static $i = 0; 156 | $opener_tag = 'opener_tag_of_balanced_tag_' . ++$i; 157 | 158 | $this->set_bookmark( $opener_tag ); 159 | if ( ! $this->next_balanced_tag_closer_tag() ) { 160 | $this->release_bookmark( $opener_tag ); 161 | return null; 162 | } 163 | 164 | $closer_tag = 'closer_tag_of_balanced_tag_' . ++$i; 165 | $this->set_bookmark( $closer_tag ); 166 | 167 | return array( $opener_tag, $closer_tag ); 168 | } 169 | 170 | /** 171 | * Finds the matching closing tag for an opening tag. 172 | * 173 | * When called while the processor is on an open tag, it traverses the HTML 174 | * until it finds the matching closer tag, respecting any in-between content, 175 | * including nested tags of the same name. Returns false when called on a 176 | * closer tag, a tag that doesn't have a closer tag (void), a tag that 177 | * doesn't visit the closer tag, or if no matching closing tag was found. 178 | * 179 | * @since 6.5.0 180 | * 181 | * @access private 182 | * 183 | * @return bool Whether a matching closing tag was found. 184 | */ 185 | public function next_balanced_tag_closer_tag(): bool { 186 | $depth = 0; 187 | $tag_name = $this->get_tag(); 188 | 189 | if ( ! $this->has_and_visits_its_closer_tag() ) { 190 | return false; 191 | } 192 | 193 | while ( $this->next_tag( 194 | array( 195 | 'tag_name' => $tag_name, 196 | 'tag_closers' => 'visit', 197 | ) 198 | ) ) { 199 | if ( ! $this->is_tag_closer() ) { 200 | ++$depth; 201 | continue; 202 | } 203 | 204 | if ( 0 === $depth ) { 205 | return true; 206 | } 207 | 208 | --$depth; 209 | } 210 | 211 | return false; 212 | } 213 | 214 | /** 215 | * Checks whether the current tag has and will visit its matching closer tag. 216 | * 217 | * @since 6.5.0 218 | * 219 | * @access private 220 | * 221 | * @return bool Whether the current tag has a closer tag. 222 | */ 223 | public function has_and_visits_its_closer_tag(): bool { 224 | $tag_name = $this->get_tag(); 225 | 226 | return null !== $tag_name && ( 227 | // @TODO: Backport the 6.5 method 228 | // ! WP_HTML_Tag_Processor::is_void( $tag_name ) && 229 | ! in_array( $tag_name, self::TAGS_THAT_DONT_VISIT_CLOSER_TAG, true ) 230 | ); 231 | } 232 | } 233 | } -------------------------------------------------------------------------------- /wp-content/html-pages/1_developer-apis/1_query-api.html: -------------------------------------------------------------------------------- 1 |

Query API

2 | 3 | 4 |

WordPress Playground exposes a simple API that you can use to configure the Playground in the browser.

5 | 6 | 7 | 8 |

It works by passing configuration options as query parameters to the Playground URL. For example, to install the pendant theme, you would use the following URL:

9 | 10 | 11 | 12 |
https://playground.wordpress.net/?theme=pendant
13 | 14 | 15 | 16 |

You can go ahead and try it out. The Playground will automatically install the theme and log you in as an admin. You may even embed this URL in your website using an <iframe> tag:

17 | 18 | 19 | 20 |
<iframe src="https://playground.wordpress.net/?theme=pendant"></iframe>
21 | 22 | 23 | 24 |

Available options

25 | 26 | 27 | 28 |
OptionDefault ValueDescription
php8.0Loads the specified PHP version. Supported values: 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3, latest
wplatestLoads the specified WordPress version. Supported values: The last three major WordPress versions. As of April 4, 2024, that's 6.3, 6.4, 6.5. You can also use these values: latest, nightly, beta
blueprint-urlThe URL of the Blueprint that will be used to configure this Playground instance.
php-extension-bundleLoads a bundle of PHP extensions. Supported bundles: kitchen-sink (for finfo, gd, mbstring, iconv, openssl, libxml, xml, dom, simplexml, xmlreader, xmlwriter), light (saves 6MB of downloads, loads none of the above extensions)
networkingyes or noEnables or disables the networking support for Playground. Defaults to no
pluginInstalls the specified plugin. Use the plugin name from the plugins directory URL, e.g. for a URL like https://wordpress.org/plugins/wp-lazy-loading/, the plugin name would be wp-lazy-loading. You can pre-install multiple plugins by saying plugin=coblocks&plugin=wp-lazy-loading&…. Installing a plugin automatically logs the user in as an admin
themeInstalls the specified theme. Use the theme name from the themes directory URL, e.g. for a URL like https://wordpress.org/themes/disco/, the theme name would be disco. Installing a theme automatically logs the user in as an admin
url/wp-admin/Load the specified initial page displaying WordPress
modeseamless, browser, or browser-full-screenDisplays WordPress on a full-page or wraps it in a browser UI
lazyDefer loading the Playground assets until someone clicks on the "Run" button
loginyesLogs the user in as an admin. Set to no to not log in.
multisitenoEnables the WordPress multisite mode.
storageSelects the storage for Playground: none gets erased on page refresh, browser is stored in the browser, and device is stored in the selected directory on a device. The last two protect the user from accidentally losing their work upon page refresh.
import-siteImports site files and database from a zip file specified by URL.
import-wxrImports site content from a WXR file specified by URL. It uses the WordPress Importer, so the default admin user must be logged in.
29 | 30 | 31 | 32 |

For example, the following code embeds a Playground with a preinstalled Gutenberg plugin, and opens the post editor:

33 | 34 | 35 | 36 |
<iframe src="https://playground.wordpress.net/?plugin=gutenberg&url=/wp-admin/post-new.php&mode=seamless"> </iframe>
37 | 38 | 39 | 40 |

:::info CORS policy

41 | 42 | 43 | 44 |

To import files from a URL, such as a site zip package, they must be served with Access-Control-Allow-Origin header set. For reference, see: Cross-Origin Resource Sharing (CORS).

45 | 46 | 47 | 48 |

:::

49 | 50 | 51 | 52 |

GitHub Export Options

53 | 54 | 55 | 56 |

The following additional query parameters may be used to pre-configure the GitHub export form:

57 | 58 | 59 | 60 | 103 | -------------------------------------------------------------------------------- /wp-content/html-pages/0_wordpress-playground/1_start-using-wordpress-playground-in-5-minutes.html: -------------------------------------------------------------------------------- 1 |

Start using WordPress Playground in 5 minutes

2 | 3 | 4 |

WordPress Playground can help you with any of the following:

5 | 6 | 7 | 8 |

This page will guide you through each of these. Oh, and if you're a visual learner – here's a video:

9 | 10 | 11 | 12 |

https://video.wordpress.com/v/3UBIXJ9S?autoPlay=false&height=1080&width=1920&fill=true

13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |

Start a new WordPress site

21 | 22 | 23 | 24 |

Every time you visit the official demo on playground.wordpress.net, you get a fresh WordPress site.

25 | 26 | 27 | 28 |

You can then create pages, upload plugins, themes, import your own site, and do most things you would do on a regular WordPress.

29 | 30 | 31 | 32 |

It's that easy to start!

33 | 34 | 35 | 36 |

The entire site lives in your browser and is scraped when you close the tab. Want to start over? Just refresh the page!

37 | 38 | 39 | 40 |
41 |

WordPress Playground is private

42 | Everything you build stays in your browser and is not sent anywhere. Once you're finished, you can export your site as a zip file. Or just refresh the page and start over!
43 | 44 | 45 | 46 |

Try a block, a theme, or a plugin

47 | 48 | 49 | 50 |

You can upload any plugin or theme you want in /wp-admin/.

51 | 52 | 53 | 54 |

To save a few clicks, you can preinstall plugins or themes from the WordPress plugin directory by adding a plugin or theme parameter to the URL. For example, to install the coblocks plugin, you can use this URL:

55 | 56 | 57 | 58 |

https://playground.wordpress.net/?plugin=coblocks

59 | 60 | 61 | 62 |

Or this URL to preinstall the pendant theme:

63 | 64 | 65 | 66 |

https://playground.wordpress.net/?theme=pendant

67 | 68 | 69 | 70 |

You can also mix and match these parameters and even add multiple plugins:

71 | 72 | 73 | 74 |

https://playground.wordpress.net/?plugin=coblocks&plugin=friends&theme=pendant

75 | 76 | 77 | 78 |

This is called Query API and you can learn more about it here.

79 | 80 | 81 | 82 |
83 |

Plugin directory doesn't work in WordPress Playground

84 | Plugins must be installed manually because your WordPress site doesn't send any data to the internet. You won't be able to navigate the WordPress plugin directory inside /wp-admin/. The Query API method may seem to contradict that, but behind the scenes it uses the same plugin upload form as you would.
85 | 86 | 87 | 88 |

Save your site

89 | 90 | 91 | 92 |

To keep your WordPress Playground site for longer than a single browser session, you can export it as a zip file.

93 | 94 | 95 | 96 |

Use the "Export" button in the top bar:

97 | 98 | 99 | 100 |
101 | 102 | 103 | 104 |

The exported file contains the complete site you've built. You could host it on any server that supports PHP and SQLite. All WordPress core files, plugins, themes, and everything else you've added to your site are in there.

105 | 106 | 107 | 108 |

The SQLite database file is also included in the export, you'll find it wp-content/database/.ht.sqlite. Keep in mind that files starting with a dot are hidden by default on most operating systems so you might need to enable the "Show hidden files" option in your file manager.

109 | 110 | 111 | 112 |

Restore a saved site

113 | 114 | 115 | 116 |

You can restore the site you saved by using the import button in WordPress Playground:

117 | 118 | 119 | 120 |
121 | 122 | 123 | 124 |

Use a specific WordPress or PHP version

125 | 126 | 127 | 128 |

The easiest way is to use the version switcher on the official demo site:

129 | 130 | 131 | 132 |
133 | 134 | 135 | 136 |
137 |

Test your plugin or theme

138 | Compatibility testing with so many WordPres and PHP versions was always a pain. WordPress Playground makes this process effortless – use it to your advantage!
139 | 140 | 141 | 142 |

You can also use the wp and php query parameters to open Playground with the right versions already loaded:

143 | 144 | 145 | 146 | 157 | 158 | 159 | 160 |

This is called Query API and you can learn more about it here.

161 | 162 | 163 | 164 |
165 |

Major versions only

166 | You can specify major versions like wp=6.2 or php=8.1 and expect the most recent release in that line. You cannot, however, request older minor versions so neither wp=6.1.2 nor php=7.4.9 will work.
167 | 168 | 169 | 170 |

Import a WXR file

171 | 172 | 173 | 174 |

You can import a WordPress export file by uploading a WXR file in /wp-admin/.

175 | 176 | 177 | 178 |

You can also use JSON Blueprints. See getting started with Blueprints to learn more.

179 | 180 | 181 | 182 |

This is different from the import feature described above. The import feature exports the entire site, including the database. This import feature imports a WXR file into an existing site.

183 | 184 | 185 | 186 |

Build apps with WordPress Playground

187 | 188 | 189 | 190 |

WordPress Playground is programmable which means you can build WordPress apps, setup plugin demos, and even use it as a zero-setup local development environment.

191 | 192 | 193 | 194 |

To learn more about developing with WordPress Playground, check out the development quick start section.

195 | -------------------------------------------------------------------------------- /wp-content/plugins/wp-docs-plugin/plugin.php: -------------------------------------------------------------------------------- 1 | get_var("SHOW TABLES LIKE 'wp_options'") != 'wp_options') { 26 | return; 27 | } 28 | initialize_docs_plugin(); 29 | }); 30 | 31 | add_filter('http_request_args', function ($args, $url) { 32 | $args['reject_unsafe_urls'] = true; 33 | return $args; 34 | }, 10, 2); 35 | 36 | add_filter('allowed_redirect_hosts', function ($deprecated = '') { 37 | return array (); 38 | }); 39 | 40 | function initialize_docs_plugin() { 41 | if(get_option('docs_populated')) { 42 | // Prevent collisions between the initial create_db_pages_from_html_files call 43 | // process and the save_post_page hook. 44 | return; 45 | } 46 | 47 | if(!file_exists(HTML_PAGES_PATH)) { 48 | return; 49 | } 50 | 51 | update_option('permalink_structure', '/%postname%/'); 52 | flush_rewrite_rules(); 53 | // Activating here because the activateTheme Blueprint step doesn't work 54 | // in wp-now :( 55 | switch_theme('playground-docs'); 56 | // Activate the gutenberg plugin and the create-block-theme plugin 57 | // for the same reasons. 58 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 59 | activate_plugin('gutenberg/gutenberg.php'); 60 | activate_plugin('create-block-theme/create-block-theme.php'); 61 | pages_reinitialize_content(); 62 | } 63 | 64 | add_action('admin_menu', function () { 65 | // Remove distracting options from the admin menu 66 | remove_menu_page('edit.php'); 67 | remove_menu_page('edit-comments.php'); 68 | remove_menu_page('users.php'); 69 | 70 | // Add a submenu under "Docs pages" menu 71 | add_submenu_page( 72 | 'edit.php?post_type=page', 73 | 'Download ZIP', 74 | 'Download ZIP', 75 | 'manage_options', 76 | 'download_docs', 77 | function () { } 78 | ); 79 | // Add a submenu under "Docs pages" menu 80 | add_submenu_page( 81 | 'edit.php?post_type=page', 82 | 'Reload doc pages from disk', 83 | 'Reload doc pages from disk', 84 | 'manage_options', 85 | 'recreate_db_pages_from_disk', 86 | function () { } 87 | ); 88 | }); 89 | 90 | add_action('admin_init', function () { 91 | if (isset($_GET['page']) && $_GET['page'] === 'download_docs') { 92 | return download_docs_callback(); 93 | } 94 | if (isset($_GET['page']) && $_GET['page'] === 'recreate_db_pages_from_disk') { 95 | pages_reinitialize_content(); 96 | 97 | // Display admin notice 98 | add_action('admin_notices', function () { 99 | echo '

Doc pages were recreated successfully.

'; 100 | }); 101 | } 102 | }); 103 | 104 | function pages_reinitialize_content() { 105 | update_option('docs_populated', false); 106 | 107 | delete_db_pages(HTML_PAGES_PATH); 108 | create_db_pages_from_html_files(HTML_PAGES_PATH); 109 | delete_db_attachments(); 110 | create_db_media_files_from_uploads(); 111 | 112 | update_option('docs_populated', true); 113 | } 114 | 115 | /** 116 | * Doc pages functions 117 | */ 118 | 119 | function download_docs_callback() { 120 | // Create a zip file of the HTML_PAGES_PATH directory 121 | $zipFile = __DIR__ . '/docs.zip'; 122 | $zip = new ZipArchive(); 123 | if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) { 124 | $files = new RecursiveIteratorIterator( 125 | new RecursiveDirectoryIterator(HTML_PAGES_PATH), 126 | RecursiveIteratorIterator::LEAVES_ONLY 127 | ); 128 | 129 | foreach ($files as $name => $file) { 130 | if (!$file->isDir()) { 131 | $filePath = $file->getRealPath(); 132 | $relativePath = substr($filePath, strlen(HTML_PAGES_PATH) + 1); 133 | $zip->addFile($filePath, $relativePath); 134 | } 135 | } 136 | 137 | $zip->close(); 138 | 139 | // Download the zip file 140 | header('Content-Type: application/zip'); 141 | header('Content-Disposition: attachment; filename="docs.zip"'); 142 | header('Content-Length: ' . filesize($zipFile)); 143 | readfile($zipFile); 144 | 145 | // Delete the zip file 146 | unlink($zipFile); 147 | } else { 148 | echo 'Failed to create zip file'; 149 | } 150 | 151 | exit; 152 | } 153 | 154 | /** 155 | * Recreate the entire file structure when any post is saved. 156 | * 157 | * Why recreate? 158 | * 159 | * It's easier to recreate the entire file structure than to keep track of 160 | * which files have been added, deleted, renamed and moved under 161 | * another parent, or changed via a direct SQL query. 162 | */ 163 | add_action('save_post_page', function ($post_id) { 164 | // Prevent collisions between the initial create_db_pages_from_html_files call 165 | // process and the save_post_page hook. 166 | if (!get_option('docs_populated')) { 167 | return; 168 | } 169 | 170 | docs_plugin_deltree(HTML_PAGES_PATH); 171 | mkdir(HTML_PAGES_PATH); 172 | save_db_pages_as_html(HTML_PAGES_PATH); 173 | }); 174 | 175 | 176 | function create_db_pages_from_html_files($dir, $parent_id = 0) { 177 | $indexFilePath = $dir . '/index.html'; 178 | if(file_exists($indexFilePath)) { 179 | $parent_id = create_db_page_from_html_file( 180 | new SplFileInfo($indexFilePath), 181 | $parent_id, 182 | get_order_from_filename(basename($dir)) 183 | ); 184 | } 185 | 186 | $files = scandir($dir); 187 | foreach ($files as $file) { 188 | if ($file === '.' || $file === '..' || $file === 'index.html') { 189 | continue; 190 | } 191 | 192 | $filePath = $dir . '/' . $file; 193 | if (is_dir($filePath)) { 194 | create_db_pages_from_html_files($filePath, $parent_id); 195 | } else if (pathinfo($file, PATHINFO_EXTENSION) === 'html') { 196 | create_db_page_from_html_file( 197 | new SplFileInfo($filePath), 198 | $parent_id, 199 | get_order_from_filename(basename($filePath)) 200 | ); 201 | } 202 | } 203 | } 204 | 205 | function get_order_from_filename($filename) { 206 | if(preg_match('/^(\d+)_/', $filename, $matches)) { 207 | return $matches[1]; 208 | } 209 | return 0; 210 | } 211 | 212 | function create_db_page_from_html_file(SplFileInfo $file, $parent_id = 0, $order = 0) { 213 | $content = file_get_contents($file->getRealPath()); 214 | $p = new Playground_Post_Export_Processor($content); 215 | $p->next_tag(); 216 | if($p->get_tag() === 'H1') { 217 | $p->set_bookmark('start'); 218 | $title = $p->get_content_between_balanced_template_tags(); 219 | $p->seek('start'); 220 | $p->remove_balanced_tag(); 221 | // Removing the tag doesn't affect the whitespace that follows, so 222 | // we need to trim the content or else we'll start accumulating leading 223 | // newlines. 224 | try { 225 | $content = trim($p->get_updated_html()); 226 | } catch(ValueError $e) { 227 | $content = ''; 228 | } 229 | // Replace placeholder site URLs with the URL of the current site. 230 | // @TODO: This is very naive, let's actually parse the block 231 | // markup and the HTML markup and make these replacements 232 | // in the JSON and HTML attributes structures, not just in 233 | // their textual representation. 234 | $content = str_replace( 235 | DOCS_INTERNAL_SITE_URL, 236 | get_site_url(), 237 | $content 238 | ); 239 | } else { 240 | $title = $file->getBasename('.html'); 241 | } 242 | 243 | // Insert the content as a WordPress page 244 | $post_data = array( 245 | 'post_title' => $title, 246 | 'post_content' => $content, 247 | 'post_status' => 'publish', 248 | 'post_author' => get_current_user_id(), 249 | 'post_type' => 'page', 250 | 'post_parent' => $parent_id, 251 | 'menu_order' => $order 252 | ); 253 | 254 | $page_id = wp_insert_post($post_data); 255 | if("0" == get_option('page_on_front')) { 256 | update_option('page_on_front', $page_id); 257 | } 258 | return $page_id; 259 | } 260 | 261 | function delete_db_pages() { 262 | $args = array( 263 | 'post_type' => 'page', 264 | 'posts_per_page' => -1, 265 | 'post_status' => 'any', 266 | ); 267 | $pages = new WP_Query($args); 268 | 269 | if ($pages->have_posts()) { 270 | while ($pages->have_posts()) { 271 | $pages->the_post(); 272 | wp_delete_post(get_the_ID(), true); 273 | } 274 | } 275 | wp_reset_postdata(); 276 | } 277 | 278 | function save_db_pages_as_html($path, $parent_id = 0) { 279 | if (!file_exists($path)) { 280 | mkdir($path, 0777, true); 281 | } 282 | 283 | $args = array( 284 | 'post_type' => 'page', 285 | 'posts_per_page' => -1, 286 | 'post_parent' => $parent_id, 287 | 'post_status' => 'publish', 288 | ); 289 | $pages = new WP_Query($args); 290 | 291 | if ($pages->have_posts()) { 292 | while ($pages->have_posts()) { 293 | $pages->the_post(); 294 | $page_id = get_the_ID(); 295 | $page = get_post($page_id); 296 | $title = sanitize_title(get_the_title()); 297 | 298 | $content = '

' . esc_html(get_the_title()) . "

\n\n" . get_the_content(); 299 | // Replace current site URL with a placeholder URL for the export. 300 | // @TODO: This is very naive, let's actually parse the block 301 | // markup and the HTML markup and make these replacements 302 | // in the JSON and HTML attributes structures, not just in 303 | // their textual representation. 304 | $content = str_replace( 305 | get_site_url(), 306 | DOCS_INTERNAL_SITE_URL, 307 | $content 308 | ); 309 | $child_pages = get_pages(array('child_of' => $page_id, 'post_type' => 'page')); 310 | 311 | if (!file_exists($path)) { 312 | mkdir($path, 0777, true); 313 | } 314 | 315 | if (!empty($child_pages)) { 316 | $new_parent = $path . '/' . $page->menu_order . '_' . $title; 317 | if (!file_exists($new_parent)) { 318 | mkdir($new_parent, 0777, true); 319 | } 320 | file_put_contents($new_parent . '/index.html', $content); 321 | save_db_pages_as_html($new_parent, $page_id); 322 | } else { 323 | file_put_contents($path . '/' . $page->menu_order . '_' . $title . '.html', $content); 324 | } 325 | } 326 | } 327 | wp_reset_postdata(); 328 | } 329 | 330 | function docs_plugin_deltree($path) { 331 | if (!file_exists($path)) { 332 | return; 333 | } 334 | 335 | $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::CHILD_FIRST); 336 | foreach ($iterator as $file) { 337 | /** @var SplFileInfo $file */ 338 | if ($file->isDir()) { 339 | rmdir($file->getRealPath()); 340 | } else if($file->isFile()) { 341 | unlink($file->getRealPath()); 342 | } 343 | } 344 | 345 | rmdir($path); 346 | } 347 | 348 | /** 349 | * Media attachments 350 | */ 351 | 352 | function delete_db_attachments() { 353 | $args = array( 354 | 'post_type' => 'attachment', 355 | 'posts_per_page' => -1, 356 | 'post_status' => 'any', 357 | ); 358 | $attachments = new WP_Query($args); 359 | 360 | add_filter('wp_delete_file', 'keep_media_file'); 361 | if ($attachments->have_posts()) { 362 | while ($attachments->have_posts()) { 363 | $attachments->the_post(); 364 | wp_delete_post(get_the_ID(), true); 365 | } 366 | } 367 | remove_filter('wp_delete_file', 'keep_media_file'); 368 | wp_reset_postdata(); 369 | } 370 | 371 | /** 372 | * Set this as a wp_delete_file filter to prevent 373 | * media files on the fisk from being deleted when 374 | * their corresponding database records are deleted. 375 | */ 376 | function keep_media_file($file) { 377 | // This function does nothing, it's just a dummy function. 378 | return ''; 379 | } 380 | 381 | function create_db_media_files_from_uploads() { 382 | $uploads = wp_upload_dir(); 383 | $uploadsDir = $uploads['basedir']; 384 | $uploadsUrl = $uploads['baseurl']; 385 | 386 | $mediaFiles = new RecursiveIteratorIterator( 387 | new RecursiveDirectoryIterator($uploadsDir), 388 | RecursiveIteratorIterator::LEAVES_ONLY 389 | ); 390 | 391 | foreach ($mediaFiles as $name => $file) { 392 | /** @var SplFileInfo $file */ 393 | $filename = $file->getFilename(); 394 | if($filename === '.gitkeep') { 395 | continue; 396 | } 397 | if (!$file->isDir()) { 398 | $filePath = $file->getRealPath(); 399 | $relativePath = substr($filePath, strlen($uploadsDir) + 1); 400 | $attachment = array( 401 | 'guid' => $uploadsUrl . '/' . $relativePath, 402 | 'post_mime_type' => naive_mime_content_type($filePath), 403 | 'post_title' => pathinfo($filePath, PATHINFO_FILENAME), 404 | 'post_content' => '', 405 | 'post_status' => 'inherit', 406 | ); 407 | if(preg_match('/^(\d+)_/', $filename, $matches)) { 408 | $attachmentId = $matches[1]; 409 | $attachment['import_id'] = $attachmentId; 410 | } 411 | $attachmentId = wp_insert_attachment($attachment, $filePath); 412 | if (!is_wp_error($attachmentId)) { 413 | require_once ABSPATH . 'wp-admin/includes/image.php'; 414 | $attachmentData = wp_generate_attachment_metadata($attachmentId, $filePath); 415 | wp_update_attachment_metadata($attachmentId, $attachmentData); 416 | } 417 | } 418 | } 419 | } 420 | 421 | // Don't generate thumbnails for images for now 422 | // so that the restore function has an easier job. 423 | // @TODO: Implement thumbnails export/import 424 | add_filter( 'intermediate_image_sizes_advanced', 'disable_image_sizes' ); 425 | 426 | function disable_image_sizes ($sizes){ 427 | unset( $sizes['thumbnail'] ); // Disable Thumbnail (150 x 150 hard cropped) 428 | unset( $sizes['medium'] ); // Disable Medium resolution (300 x 300 max height 300px) 429 | unset( $sizes['medium_large'] ); // Disable Medium Large (added in WP 4.4) resolution (768 x 0 infinite height) 430 | unset( $sizes['large'] ); // Disable Large resolution (1024 x 1024 max height 1024px) 431 | 432 | return $sizes; 433 | } 434 | 435 | /** 436 | * Workaround in wp-now where the finfo PHP extension 437 | * is not installed yet. Let's replace this with 438 | * mime_content_type once a new version is released. 439 | * 440 | * @param mixed $path 441 | * @return string 442 | */ 443 | function naive_mime_content_type($path) { 444 | $extension = pathinfo($path, PATHINFO_EXTENSION); 445 | switch ($extension) { 446 | case 'jpg': 447 | case 'jpeg': 448 | return 'image/jpeg'; 449 | case 'png': 450 | return 'image/png'; 451 | case 'gif': 452 | return 'image/gif'; 453 | case 'pdf': 454 | return 'application/pdf'; 455 | default: 456 | return 'application/octet-stream'; 457 | } 458 | } 459 | 460 | /** 461 | * The image block stores the attachment ID so we need 462 | * to preserve it in the export. Let's prepend it to the 463 | * filename so that we can restore it later. 464 | * 465 | * @param mixed $file 466 | * @return mixed 467 | */ 468 | function rename_uploaded_file($attachment_id) { 469 | // Do not rename the attachments when importing. 470 | if (!get_option('docs_populated')) { 471 | return; 472 | } 473 | $file = get_attached_file($attachment_id); 474 | $path = pathinfo($file); 475 | 476 | // new filename structure here 477 | $newfilename = $attachment_id . "_" . $path['filename']; 478 | $newfile = $path['dirname']."/".$newfilename.".".$path['extension']; 479 | 480 | rename($file, $newfile); 481 | update_attached_file($attachment_id, $newfile); 482 | } 483 | add_action('add_attachment', 'rename_uploaded_file'); 484 | --------------------------------------------------------------------------------