├── .browserslistrc ├── .distignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── deploy-to-dotorg.yml │ ├── pr-checks.yml │ └── release-new-version.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.json ├── .wordpress-org ├── banner-1544x500.png ├── banner-772x250.png ├── blueprints │ └── blueprint.json ├── icon-128x128.png ├── icon-256x256.png ├── icon-512x512.png ├── screenshot-1.jpg ├── screenshot-2.jpg ├── screenshot-3.jpg ├── screenshot-4.jpg ├── screenshot-5.jpg ├── screenshot-6.jpg └── screenshot-7.jpg ├── .wp-env.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── boilerplate │ ├── parts │ │ ├── footer.html │ │ └── header.html │ ├── screenshot.png │ ├── templates │ │ └── index.html │ └── theme.json ├── faq_fonts.webp ├── faq_icon.webp ├── faq_save.webp └── header_logo.webp ├── babel.config.js ├── bin └── install-wp-tests.sh ├── composer.json ├── composer.lock ├── create-block-theme.php ├── includes ├── class-create-block-theme-admin-landing.php ├── class-create-block-theme-api.php ├── class-create-block-theme-editor-tools.php ├── class-create-block-theme-loader.php ├── class-create-block-theme.php ├── create-theme │ ├── cbt-zip-archive.php │ ├── resolver_additions.php │ ├── theme-create.php │ ├── theme-fonts.php │ ├── theme-json.php │ ├── theme-locale.php │ ├── theme-media.php │ ├── theme-patterns.php │ ├── theme-readme.php │ ├── theme-styles.php │ ├── theme-tags.php │ ├── theme-templates.php │ ├── theme-token-processor.php │ ├── theme-utils.php │ └── theme-zip.php └── index.php ├── index.php ├── package-lock.json ├── package.json ├── phpcs.xml.dist ├── phpunit.xml.dist ├── readme.txt ├── src ├── admin-landing-page.js ├── admin-landing-page.scss ├── editor-sidebar │ ├── about.js │ ├── create-panel.js │ ├── create-variation-panel.js │ ├── global-styles-json-editor-modal.js │ ├── json-editor-modal.js │ ├── metadata-editor-modal.js │ ├── reset-theme.js │ ├── save-panel.js │ └── screen-header.js ├── landing-page │ ├── create-modal.js │ └── landing-page.js ├── lib │ └── lib-font │ │ ├── inflate.js │ │ ├── lib-font.browser.js │ │ └── unbrotli.js ├── plugin-sidebar.js ├── plugin-styles.scss ├── resolvers.js ├── test │ └── unit.js └── utils │ ├── download-file.js │ ├── fonts.js │ └── generate-versions.js ├── test └── unit │ ├── jest.config.js │ └── setup.js ├── tests ├── CbtThemeLocale │ ├── base.php │ ├── escapeAttribute.php │ ├── escapeTextContent.php │ └── escapeTextContentOfBlocks.php ├── CbtThemeReadme │ ├── addOrUpdateSection.php │ ├── base.php │ ├── create.php │ ├── filePath.php │ ├── getContent.php │ └── update.php ├── bootstrap.php ├── data │ ├── fonts │ │ ├── OpenSans-Regular.otf │ │ ├── OpenSans-Regular.ttf │ │ ├── OpenSans-Regular.woff │ │ └── OpenSans-Regular.woff2 │ └── themes │ │ ├── test-theme-locale │ │ ├── style.css │ │ └── theme.json │ │ └── test-theme-readme │ │ ├── readme.txt │ │ ├── style.css │ │ └── theme.json ├── test-theme-fonts.php ├── test-theme-media.php ├── test-theme-templates.php └── test-theme-utils.php ├── update-version-and-changelog.js └── webpack.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | extends @wordpress/browserslist-config 2 | -------------------------------------------------------------------------------- /.distignore: -------------------------------------------------------------------------------- 1 | # Files to be ignored in the plugin release 2 | 3 | node_modules/ 4 | vendor/ 5 | .gitignore 6 | update-google-fonts-json-file.js 7 | update-version-and-changelog.js 8 | package.json 9 | src/ 10 | test/ 11 | .git 12 | .github 13 | .wordpress-org 14 | .browserslistrc 15 | .distignore 16 | .editorconfig 17 | .eslintignore 18 | .eslintrc.json 19 | .husky/ 20 | .nvmrc 21 | .prettierignore 22 | .prettierrc.js 23 | .stylelintignore 24 | .stylelintrc.json 25 | .wp-env.json 26 | CONTRIBUTING.md 27 | README.md 28 | babel.config.js 29 | composer.json 30 | composer.lock 31 | package-lock.json 32 | webpack.config.js 33 | phpcs.xml.dist 34 | phpunit.xml.dist 35 | tests/ 36 | bin/ 37 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | 16 | [*.{yml,yaml}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.{gradle,java,kt}] 21 | indent_style = space 22 | 23 | [packages/react-native-*/**.xml] 24 | indent_style = space 25 | 26 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | vendor 2 | node_modules 3 | build 4 | admin/js/lib 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:@wordpress/eslint-plugin/recommended", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "@wordpress/dependency-group": "error", 8 | "@wordpress/i18n-text-domain": [ 9 | "error", 10 | { 11 | "allowedTextDomain": "create-block-theme" 12 | } 13 | ], 14 | "react/jsx-boolean-value": "error", 15 | "unicorn/no-abusive-eslint-disable": "error" 16 | }, 17 | "ignorePatterns": [ "src/lib" ], 18 | "plugins": [ "unicorn" ], 19 | "overrides": [ 20 | { 21 | "files": [ "**/test/**/*.js" ], 22 | "extends": [ "plugin:@wordpress/eslint-plugin/test-unit" ] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-dotorg.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to WordPress.org 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | branches: 7 | - trunk 8 | 9 | jobs: 10 | deploy-to-wordpress: 11 | if: > 12 | github.event_name == 'pull_request' && 13 | github.event.pull_request.merged == true && 14 | startsWith(github.event.pull_request.head.ref, 'release/') && 15 | ( contains(github.event.pull_request.head.ref, '/major') || contains(github.event.pull_request.head.ref, '/minor') || contains(github.event.pull_request.head.ref, '/patch') ) && 16 | ( github.event.pull_request.user.login == 'github-actions[bot]' ) 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 20 25 | - name: Install node dependencies 26 | run: npm install 27 | 28 | - name: Compile JavaScript App 29 | run: npm run build 30 | 31 | - name: Get New Version 32 | id: get-version 33 | run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 34 | 35 | - name: Create Tag and Release on GitHub 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | run: | 39 | VERSION=v${{ steps.get-version.outputs.VERSION }} 40 | git tag $VERSION 41 | git push origin $VERSION 42 | gh release create $VERSION --generate-notes 43 | 44 | - name: Deploy Plugin to WordPress Plugin Directory 45 | uses: 10up/action-wordpress-plugin-deploy@stable 46 | env: 47 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 48 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 49 | VERSION: ${{ steps.get-version.outputs.VERSION }} 50 | 51 | - name: WordPress.org plugin asset/readme update 52 | uses: 10up/action-wordpress-plugin-asset-update@stable 53 | env: 54 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 55 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 56 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: Run checks 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - trunk 7 | jobs: 8 | lint: 9 | name: Lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Setup Node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version-file: '.nvmrc' 19 | cache: npm 20 | 21 | - name: Install dependencies 22 | uses: php-actions/composer@v6 23 | with: 24 | args: --ignore-platform-reqs 25 | 26 | - name: Install Dependencies 27 | run: npm i 28 | 29 | - name: Run PHP Lint 30 | run: npm run lint:php 31 | 32 | - name: Run JS Lint 33 | if: success() || failure() 34 | run: npm run lint:js 35 | 36 | - name: Run CSS Lint 37 | if: success() || failure() 38 | run: npm run lint:css 39 | 40 | - name: Run Markdown Lint 41 | if: success() || failure() 42 | run: npm run lint:md-docs 43 | 44 | - name: Run package.json Lint 45 | if: success() || failure() 46 | run: npm run lint:pkg-json 47 | 48 | tests: 49 | name: Test Suite 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v3 54 | 55 | - name: Setup Node 56 | uses: actions/setup-node@v3 57 | with: 58 | node-version-file: '.nvmrc' 59 | cache: npm 60 | 61 | - name: Install Composer 62 | uses: php-actions/composer@v6 63 | with: 64 | args: --ignore-platform-reqs 65 | 66 | - name: Install Node Dependencies 67 | run: npm i 68 | 69 | - name: Compile JavaScript App 70 | run: npm run build 71 | 72 | - name: Setup MySQL 73 | if: success() || failure() 74 | uses: shogo82148/actions-setup-mysql@v1 75 | with: 76 | mysql-version: '8.0' 77 | 78 | - name: Run JavaScript unit tests 79 | run: npm run test:unit 80 | 81 | - name: Install Subversion 82 | run: sudo apt-get update -y && sudo apt-get install -y subversion 83 | 84 | - name: Run PHP tests 85 | run: | 86 | mysql -uroot -h127.0.0.1 -e 'SELECT version()' \ 87 | && ./bin/install-wp-tests.sh --wp-version=trunk --recreate-db wordpress_test root '' > /dev/null \ 88 | && composer run-script test 89 | -------------------------------------------------------------------------------- /.github/workflows/release-new-version.yml: -------------------------------------------------------------------------------- 1 | name: Create new release PR 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_type: 7 | description: 'Release type' 8 | required: true 9 | type: choice 10 | options: 11 | - major 12 | - minor 13 | - patch 14 | 15 | jobs: 16 | prepare-release: 17 | if: github.event_name == 'workflow_dispatch' 18 | name: Prepare Release PR 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: 20 28 | 29 | - name: Install node dependencies 30 | run: npm install 31 | 32 | - name: Compile Javascript App 33 | run: npm run build 34 | 35 | - name: Create version update branch 36 | id: create-branch 37 | run: | 38 | BRANCH_NAME="release/$(date +%Y-%m-%d)/${{ github.event.inputs.release_type }}-release" 39 | git checkout -b $BRANCH_NAME 40 | echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT 41 | 42 | - name: Update version and changelog 43 | id: update-version 44 | run: | 45 | npm run update-version 46 | echo "NEW_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 47 | env: 48 | RELEASE_TYPE: ${{ github.event.inputs.release_type }} 49 | 50 | - name: Commit changes 51 | run: | 52 | git config user.name 'github-actions[bot]' 53 | git config user.email 'github-actions[bot]@users.noreply.github.com' 54 | git add . 55 | git commit -m "Version bump & changelog update" --no-verify 56 | git push --set-upstream origin ${{ steps.create-branch.outputs.BRANCH_NAME }} 57 | 58 | - name: Create Pull Request 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | run: | 62 | gh pr create \ 63 | --title "[Automation] New ${{ github.event.inputs.release_type }} Release: ${{ steps.update-version.outputs.NEW_VERSION }}" \ 64 | --base trunk \ 65 | --head ${{ steps.create-branch.outputs.BRANCH_NAME }} \ 66 | --label "Release: ${{ github.event.inputs.release_type }}" \ 67 | --body " 68 | ### Release PR 🤖 69 | This is a release PR for version **${{ steps.update-version.outputs.NEW_VERSION }}**, run by **@${{ github.actor }}**. 70 | It updates the version of the Plugin and adds changes since the last tag to the Changelog file. 71 | Merging this PR will trigger a new release and update the Plugin in the WordPress Plugin Directory." 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore everything in the root except the "wp-content" directory. 2 | !wp-content/ 3 | 4 | # ignore everything in the "wp-content" directory, except: 5 | # "mu-plugins", "plugins", "themes" directory 6 | wp-content/* 7 | !wp-content/mu-plugins/ 8 | !wp-content/plugins/ 9 | !wp-content/themes/ 10 | 11 | # ignore these plugins 12 | wp-content/plugins/hello.php 13 | 14 | # ignore specific themes 15 | wp-content/themes/twenty*/ 16 | 17 | # ignore dependency directories 18 | node_modules/ 19 | vendor/ 20 | 21 | # ignore log files and databases 22 | *.log 23 | *.sql 24 | *.sqlite 25 | 26 | # ignore system files 27 | .DS_Store 28 | .vscode 29 | 30 | # plugin build folder 31 | build/ 32 | 33 | # Ignore local override files 34 | .wp-env.override.json 35 | 36 | # phpunit 37 | *.result.* 38 | 39 | dev-env/ 40 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | vendor 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // Import the default config file and expose it in the project root. 2 | // Useful for editor integrations. 3 | module.exports = require( '@wordpress/prettier-config' ); 4 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | vendor 4 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@wordpress/stylelint-config/scss", 3 | "rules": { 4 | "at-rule-empty-line-before": null, 5 | "at-rule-no-unknown": null, 6 | "comment-empty-line-before": null, 7 | "font-weight-notation": null, 8 | "max-line-length": null, 9 | "no-descending-specificity": null, 10 | "rule-empty-line-before": null, 11 | "selector-class-pattern": null, 12 | "value-keyword-case": null, 13 | "scss/operator-no-unspaced": null, 14 | "scss/selector-no-redundant-nesting-selector": null, 15 | "scss/at-import-partial-extension": null, 16 | "scss/no-global-function-names": null, 17 | "scss/comment-no-empty": null, 18 | "scss/at-extend-no-missing-placeholder": null, 19 | "scss/operator-no-newline-after": null, 20 | "scss/at-if-closing-brace-newline-after": null, 21 | "scss/at-else-empty-line-before": null, 22 | "scss/at-if-closing-brace-space-after": null, 23 | "no-invalid-position-at-import-rule": null 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.wordpress-org/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/banner-1544x500.png -------------------------------------------------------------------------------- /.wordpress-org/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/banner-772x250.png -------------------------------------------------------------------------------- /.wordpress-org/blueprints/blueprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json", 3 | "landingPage": "/wp-admin/themes.php?page=create-block-theme-landing", 4 | "features": { 5 | "networking": true 6 | }, 7 | "steps": [ 8 | { 9 | "step": "login", 10 | "username": "admin", 11 | "password": "password" 12 | }, 13 | { 14 | "step": "installPlugin", 15 | "pluginZipFile": { 16 | "resource": "wordpress.org/plugins", 17 | "slug": "create-block-theme" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.wordpress-org/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/icon-128x128.png -------------------------------------------------------------------------------- /.wordpress-org/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/icon-256x256.png -------------------------------------------------------------------------------- /.wordpress-org/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/icon-512x512.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/screenshot-1.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/screenshot-2.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/screenshot-3.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/screenshot-4.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/screenshot-5.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/screenshot-6.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/.wordpress-org/screenshot-7.jpg -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/wp-env.json", 3 | "core": "WordPress/WordPress", 4 | "port": 8988, 5 | "testsPort": 8989, 6 | "plugins": [ "." ], 7 | "config": { 8 | "WP_UPLOAD_MAX_FILESIZE": "128M", 9 | "WP_MEMORY_LIMIT": "256M", 10 | "WP_ENVIRONMENT_TYPE": "development", 11 | "WP_DEVELOPMENT_MODE": "all" 12 | }, 13 | "env": { 14 | "development": { 15 | "phpmyadminPort": 9000 16 | }, 17 | "tests": { 18 | "phpmyadminPort": 9001 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome to Create Block Theme! All are welcome here. 4 | 5 | ## How can I contribute? 6 | 7 | We welcome contributions in all forms, including code, design, documentation, and triage. 8 | 9 | ### Discussions 10 | 11 | Contributors can be found in the [#core-editor](https://make.wordpress.org/chat/) and [#outreach](https://wordpress.slack.com/archives/C015GUFFC00) channels in [Make WordPress Slack](https://make.wordpress.org/chat). 12 | 13 | There is also a [GitHub project board](https://github.com/orgs/WordPress/projects/188) which is used to plan and track the progress of the plugin. 14 | 15 | ### Development Setup 16 | 17 | The basic setup for development is: 18 | 19 | - Node/NPM Development Tools 20 | - WordPress Development Site 21 | - Code Editor 22 | 23 | #### Prerequisites 24 | 25 | - [Node.js](https://nodejs.org/en/) (>= v20.10.0) 26 | - [Composer](https://getcomposer.org/) (used for linting PHP) 27 | - WordPress Development Site, such as [wp-env](https://github.com/WordPress/gutenberg/blob/trunk/packages/env/README.md) or [Local](https://localwp.com/) 28 | - We recommend using [Node Version Manager](https://github.com/nvm-sh/nvm) (nvm) to manage your Node.js versions 29 | 30 | We recommend following the [Gutenberg code contribution guide](https://github.com/WordPress/gutenberg/blob/trunk/docs/contributors/code/getting-started-with-code-contribution.md) for more details on setting up a development environment. 31 | 32 | #### Code Setup 33 | 34 | [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the Create Block Theme repository, [clone it to your computer](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) and add the WordPress repository as [upstream](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/configuring-a-remote-repository-for-a-fork) 35 | 36 | ```bash 37 | git clone https://github.com/YOUR_GITHUB_USERNAME/create-block-theme.git 38 | cd create-block-theme 39 | git remote add upstream https://github.com/WordPress/create-block-theme.git 40 | ``` 41 | 42 | Run the following commands to install the plugin dependencies: 43 | 44 | ```bash 45 | npm install 46 | composer install 47 | ``` 48 | 49 | Run `npm run build` to build the plugin. 50 | 51 | There are several linter commands available to help ensure the plugin follows the WordPress coding standards: 52 | 53 | - CSS: `npm run lint:css` & `npm run lint:css:fix` 54 | - JS: `npm run lint:js` & `npm run lint:js:fix` 55 | - PHP: `npm run lint:php` & `npm run lint:php:fix` 56 | 57 | To test a WordPress plugin, you need to have WordPress itself installed. If you already have a WordPress environment setup, use the above Create Block Theme build as a standard WordPress plugin by putting the `create-block-theme` directory in your wp-content/plugins/ directory. 58 | 59 | ### Repository Management 60 | 61 | Members of the [Block Themers GitHub team](https://github.com/orgs/WordPress/teams/block-themers) have write access to the repository. The team is made up of contributors who have: 62 | 63 | - Demonstrated a commitment to improving how block themes are built in the editor 64 | - Made 2-3 meaningful contributions to the plugin (similar to the [Gutenberg team requirements](https://developer.wordpress.org/block-editor/contributors/repository-management/#teams)) 65 | 66 | If you meet this criteria and would like to be added to the Block Themers team, feel free to ask in the [#core-editor](https://make.wordpress.org/chat/) Slack channel. 67 | 68 | If you are not a member of the team, you can still contribute by forking the repository and submitting a pull request. 69 | 70 | ## Releasing a new version of Create Block Theme 71 | 72 | We have an automated process for the release of new versions of Create Block Theme to the public. 73 | 74 | ### 1 - Initiate the Release Process 75 | 76 | To begin the release process, execute the [**Create new release PR**](https://github.com/WordPress/create-block-theme/actions/workflows/release-new-version.yml) workflow from the Actions tab. Choose the type of release — major, minor, or patch — from the "Run workflow" dropdown menu. This action triggers the creation of a new Release PR, such as [#592](https://github.com/WordPress/create-block-theme/pull/592/files), which includes an automated version bump and proposed changes to the Change Log. 77 | 78 | ### 2 - Update the Release PR 79 | 80 | Keep the Release PR current by incorporating any new changes from the `trunk` that are intended for this release. Use the `git cherry-pick [commit-hash]` command to add specific commits to the Release Branch associated with the Release PR. The Release Branch is named using the format: `release/[creation-date]/[release-type]-release`, where `[creation-date]` is the date the Release PR was created, and `[release-type]` is the type selected during the workflow initiation. 81 | 82 | ### 3 - Finalize the Release 83 | 84 | Once the release is deemed complete and ready, it must be reviewed and approved by members of the organization. Following approval, the Release PR is merged into the main branch. This action triggers the [**Deploy to Dotorg**](https://github.com/WordPress/create-block-theme/actions/workflows/deploy-to-dotorg.yml) workflow, which tags the release on both GitHub and the WordPress Plugin Directory SVN. A Release Confirmation is then triggered on WordPress.org, notifying plugin maintainers via email of the new release awaiting confirmation. Upon confirmation, the new version becomes live on the WordPress Plugin Directory. 85 | 86 | ## Guidelines 87 | 88 | - 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](https://make.wordpress.org/handbook/community-code-of-conduct/). 89 | 90 | - Contributors should follow WordPress' [coding standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/) and [accessibility coding standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/accessibility/). 91 | 92 | - You maintain copyright over any contribution you make. By submitting a pull request you agree to release that code under the [plugin's License](/LICENSE.md). 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create Block Theme 2 | 3 | Welcome to Create Block Theme - a WordPress plugin to create block themes from within the Editor. 4 | 5 | It works alongside features that are already available in the Editor to enhance the workflow for creating block themes. After being tested in this plugin, some of the features included here may be moved into the Editor itself. 6 | 7 | > [!IMPORTANT] 8 | > *Disclaimer:* The Create Block Theme plugin offers critical developer-friendly features; you can think of it as a Development Mode for WordPress, and you should *keep in mind that changes made through this plugin could change your site and/or theme permanently*. 9 | > 10 | > (Make sure you know what you're doing before hitting that 'Save' button 😉) 11 | 12 | This plugin allows you to: 13 | 14 | - Export your existing theme with all customizations made in the Editor 15 | - Create a new theme, blank theme, child theme, or style variation from the Editor 16 | 17 | This plugin also makes several changes to the contents of an exported theme, including: 18 | 19 | - Adds all images used in templates to the theme's `assets` folder. 20 | - Ensures the block markup used in templates and patterns is export-ready. 21 | - Ensures most strings used in templates and patterns are translate-ready. 22 | 23 | Learn more about Create Block Theme: 24 | 25 | - [How to use the plugin](#how-to-use-the-plugin) 26 | - [How to contribute](#how-to-contribute) 27 | - [User FAQs](https://wordpress.org/plugins/create-block-theme/) 28 | 29 | ## User Support 30 | 31 | If you have run into an issue, you should check the [Support Forums](https://wordpress.org/support/plugin/create-block-theme/) first. The forums are a great place to get help. If you have a bug to report, please submit it to this repository as [an issue](https://github.com/WordPress/create-block-theme/issues). Please search prior to creating a new bug to confirm its not a duplicate. 32 | 33 | ## Plugin Features 34 | 35 | ### Theme Creation Options 36 | 37 | There are six options the plugin provides to create a new theme: 38 | 39 | #### 1. Export 40 | 41 | Export the activated theme including the user's changes. 42 | 43 | #### 2. Create a child theme 44 | 45 | Creates a new child theme with the currently active theme as a parent. 46 | 47 | #### 3. Clone the active theme 48 | 49 | Creates a new theme by cloning the activated theme. The resulting theme will have all of the assets of the activated theme combined with the user's changes. 50 | 51 | #### 4. Overwrite theme files 52 | 53 | Saves the user's changes to the theme files and deletes the user's changes from the site. 54 | 55 | #### 5. Generate blank theme 56 | 57 | Generate a boilerplate "empty" theme inside of the current site's themes directory. 58 | 59 | #### 6. Create a style variation 60 | 61 | Saves user's changes as a [style variation](https://developer.wordpress.org/themes/advanced-topics/theme-json/#global-styles-variations) of the currently active theme. 62 | 63 | ### Embed Fonts 64 | 65 | Save fonts in your theme that have been installed with the Font Library (found in WordPress 6.5+, [more information](https://wordpress.org/documentation/wordpress-version/version-6-5/#add-and-manage-fonts-across-your-site)). 66 | 67 | ## How to Use the Plugin 68 | 69 | ### Step 1: Setup 70 | 71 | To use the latest release of the Create Block Theme plugin on your WordPress site: install from the plugins page in wp-admin, or [download from the WordPress.org plugins repository](https://wordpress.org/plugins/create-block-theme). 72 | 73 | There will be a new panel accessible from the WordPress Editor, which you can open by clicking on a new icon to the right of the "Save" button, at the top of the Editor. 74 | 75 | In the WordPress Admin Dashboard, under Appearance there will also be a new page called "Create Block Theme". 76 | 77 | ### Step 2: Styles and templates customizations 78 | 79 | Make changes to your site styles, fonts and templates using the Editor. 80 | 81 | ### Step 3: Save 82 | 83 | Still in the WordPress Editor, navigate to the Create Block Theme menu at the top of the Editor. 84 | 85 | To save recent changes made in the Editor to the currently active theme or export the theme: 86 | 87 | - Select "Save Changes to Theme" and select any options to customize what is saved 88 | - Check "Save Fonts" to copy the assets for any fonts installed and activated through the Font Library to the active font 89 | - Check "Save Style Changes" to copy your style changes made to the theme.json file 90 | - Check "Save Template Changes" to copy template changes made in the Editor to your activated theme. 91 | - With "Save Template Changes you may also select the following: 92 | - Check "Localize Text" to copy content to patterns from templates so that they can be localized for internationalization. 93 | - Check "Localize Images" to copy any images referenced in templates to the theme asset folder and reference them from a pattern. 94 | - Check "Remove Navigation Refs" to remove any navigation ref IDs from templates. 95 | - Click "Save Changes" to save any recent changes to the currently active theme. 96 | 97 | To export your theme to a zip file ready to import into another system: 98 | 99 | - Select "Export Zip" 100 | 101 | To edit the theme metadata: 102 | 103 | - Select "Edit Theme Metadata" to edit the metadata for the theme. These details will be used in the style.css file. 104 | 105 | To create a new blank theme: 106 | 107 | - Select "Create Blank Theme" 108 | - Supply a name for the new theme (and optional additional Metadata) 109 | - Click "Create Blank Theme" 110 | 111 | The theme will be created and activated. 112 | 113 | To create a variation: 114 | 115 | - Select "Create Theme Variation" 116 | - Provide a name for the new Variation 117 | - Click "Create Theme Variation" 118 | 119 | A new variation will be created. 120 | 121 | To create a new Clone of the current theme or to create a Child of the current theme: 122 | 123 | - Click "Create Theme" 124 | - Click "Clone Theme" to create a new Theme based on the active theme with your changes 125 | - Click "Create Child Theme" to create a new Child Theme with the active theme as a parent with your changes 126 | 127 | To inspect the active theme's theme.json contents: 128 | 129 | - Select "Inspect Theme JSON" 130 | 131 | Many of these options are also available under the older, deprecated Create Block Theme page under Appearance > Create Block Theme. 132 | 133 | To install and uninstall fonts: 134 | 135 | - Install and activate a font from any source using the WordPress Font Library. 136 | - Select "Save Changes" to save all of the active fonts to the currently active theme. These fonts will then be activated in the theme and deactivated in the system (and may be safely deleted from the system). 137 | - Any fonts that are installed in the theme that have been deactivated with the WordPress Font Library will be removed from the theme. 138 | 139 | ## How to Contribute 140 | 141 | We welcome contributions in all forms, including code, design, documentation, and triage. Please see our [Contributing Guidelines](/CONTRIBUTING.md) for more information. 142 | -------------------------------------------------------------------------------- /assets/boilerplate/parts/footer.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |

Proudly Powered by WordPress

7 | 8 |
9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /assets/boilerplate/parts/header.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 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /assets/boilerplate/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/assets/boilerplate/screenshot.png -------------------------------------------------------------------------------- /assets/boilerplate/templates/index.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 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /assets/boilerplate/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/theme.json", 3 | "settings": { 4 | "appearanceTools": true, 5 | "layout": { 6 | "contentSize": "620px", 7 | "wideSize": "1000px" 8 | }, 9 | "spacing": { 10 | "units": [ "%", "px", "em", "rem", "vh", "vw" ] 11 | }, 12 | "typography": { 13 | "fontFamilies": [ 14 | { 15 | "fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif", 16 | "name": "System Font", 17 | "slug": "system-font" 18 | } 19 | ] 20 | }, 21 | "useRootPaddingAwareAlignments": true 22 | }, 23 | "templateParts": [ 24 | { 25 | "area": "header", 26 | "name": "header" 27 | }, 28 | { 29 | "area": "footer", 30 | "name": "footer" 31 | } 32 | ], 33 | "version": 3 34 | } 35 | -------------------------------------------------------------------------------- /assets/faq_fonts.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/assets/faq_fonts.webp -------------------------------------------------------------------------------- /assets/faq_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/assets/faq_icon.webp -------------------------------------------------------------------------------- /assets/faq_save.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/assets/faq_save.webp -------------------------------------------------------------------------------- /assets/header_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/create-block-theme/654714b143d1df50079dcc2950423e705122324d/assets/header_logo.webp -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ( api ) => { 2 | api.cache( true ); 3 | 4 | return { 5 | presets: [ '@wordpress/babel-preset-default' ], 6 | plugins: [ '@emotion/babel-plugin', 'babel-plugin-inline-json-import' ], 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | # Forked from https://github.com/wp-cli/scaffold-command/blob/main/features/install-wp-tests.feature 2 | # Function to display usage instructions 3 | display_usage() { 4 | echo "Usage: $0 [options] [db-host] [skip-database-creation]" 5 | echo "Options:" 6 | echo " --recreate-db Recreate the database" 7 | echo " --wp-version The WordPress version to install. Default is 'latest'." 8 | echo " --help Displays this help message" 9 | } 10 | 11 | RECREATE_DB=0 12 | 13 | # Parse command-line arguments 14 | for arg in "$@" 15 | do 16 | case $arg in 17 | --wp-version=*) 18 | WP_VERSION="${arg#*=}" 19 | shift 20 | ;; 21 | --recreate-db) 22 | RECREATE_DB=1 23 | shift 24 | ;; 25 | --help) 26 | SHOW_HELP=1 27 | shift 28 | ;; 29 | esac 30 | done 31 | 32 | if [[ "$SHOW_HELP" -eq 1 ]] 33 | then 34 | display_help 35 | exit 0 36 | fi 37 | 38 | # Check if required arguments are provided 39 | if [[ $# -lt 3 ]]; then 40 | display_usage 41 | exit 1 42 | fi 43 | 44 | DB_NAME=$1 45 | DB_USER=$2 46 | DB_PASS=$3 47 | DB_HOST=${4-127.0.0.1} 48 | SKIP_DB_CREATE=${6-false} 49 | 50 | TMPDIR=${TMPDIR-/tmp} 51 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 52 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 53 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} 54 | 55 | download() { 56 | if [ `which curl` ]; then 57 | curl -s "$1" > "$2"; 58 | elif [ `which wget` ]; then 59 | wget -nv -O "$2" "$1" 60 | fi 61 | } 62 | 63 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 64 | WP_BRANCH=${WP_VERSION%\-*} 65 | WP_TESTS_TAG="branches/$WP_BRANCH" 66 | 67 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 68 | WP_TESTS_TAG="branches/$WP_VERSION" 69 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 70 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 71 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 72 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 73 | else 74 | WP_TESTS_TAG="tags/$WP_VERSION" 75 | fi 76 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 77 | WP_TESTS_TAG="trunk" 78 | else 79 | # http serves a single offer, whereas https serves multiple. we only want one 80 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 81 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 82 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 83 | if [[ -z "$LATEST_VERSION" ]]; then 84 | echo "Latest WordPress version could not be found" 85 | exit 1 86 | fi 87 | WP_TESTS_TAG="tags/$LATEST_VERSION" 88 | fi 89 | set -ex 90 | 91 | install_wp() { 92 | 93 | if [ -d $WP_CORE_DIR ]; then 94 | return; 95 | fi 96 | 97 | mkdir -p $WP_CORE_DIR 98 | 99 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 100 | mkdir -p $TMPDIR/wordpress-trunk 101 | rm -rf $TMPDIR/wordpress-trunk/* 102 | svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress 103 | mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR 104 | else 105 | if [ $WP_VERSION == 'latest' ]; then 106 | local ARCHIVE_NAME='latest' 107 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 108 | # https serves multiple offers, whereas http serves single. 109 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 110 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 111 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 112 | LATEST_VERSION=${WP_VERSION%??} 113 | else 114 | # otherwise, scan the releases and get the most up to date minor version of the major release 115 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 116 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 117 | fi 118 | if [[ -z "$LATEST_VERSION" ]]; then 119 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 120 | else 121 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 122 | fi 123 | else 124 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 125 | fi 126 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 127 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 128 | fi 129 | 130 | download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 131 | } 132 | 133 | install_test_suite() { 134 | # portable in-place argument for both GNU sed and Mac OSX sed 135 | if [[ $(uname -s) == 'Darwin' ]]; then 136 | local ioption='-i.bak' 137 | else 138 | local ioption='-i' 139 | fi 140 | 141 | # set up testing suite if it doesn't yet exist 142 | if [ ! -d $WP_TESTS_DIR ]; then 143 | # set up testing suite 144 | mkdir -p $WP_TESTS_DIR 145 | rm -rf $WP_TESTS_DIR/{includes,data} 146 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 147 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 148 | fi 149 | 150 | if [ ! -f wp-tests-config.php ]; then 151 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 152 | # remove all forward slashes in the end 153 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 154 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 155 | sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 156 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 157 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 158 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 159 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 160 | fi 161 | 162 | } 163 | 164 | recreate_db() { 165 | shopt -s nocasematch 166 | if [[ $1 =~ ^(y|yes)$ ]] 167 | then 168 | mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA 169 | create_db 170 | echo "Recreated the database ($DB_NAME)." 171 | else 172 | echo "Leaving the existing database ($DB_NAME) in place." 173 | fi 174 | shopt -u nocasematch 175 | } 176 | 177 | create_db() { 178 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 179 | } 180 | 181 | install_db() { 182 | 183 | if [ ${SKIP_DB_CREATE} = "true" ]; then 184 | return 0 185 | fi 186 | 187 | # parse DB_HOST for port or socket references 188 | local PARTS=(${DB_HOST//\:/ }) 189 | local DB_HOSTNAME=${PARTS[0]}; 190 | local DB_SOCK_OR_PORT=${PARTS[1]}; 191 | local EXTRA="" 192 | 193 | if ! [ -z $DB_HOSTNAME ] ; then 194 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 195 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 196 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 197 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 198 | elif ! [ -z $DB_HOSTNAME ] ; then 199 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 200 | fi 201 | fi 202 | 203 | # create database 204 | if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] 205 | then 206 | if [[ "$RECREATE_DB" -eq 1 ]] 207 | then 208 | DELETE_EXISTING_DB='y' 209 | else 210 | echo "Reinstalling will delete the existing test database ($DB_NAME)" 211 | read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB 212 | fi 213 | recreate_db $DELETE_EXISTING_DB 214 | else 215 | create_db 216 | fi 217 | } 218 | 219 | install_wp 220 | install_test_suite 221 | install_db 222 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress/create-block-theme", 3 | "type": "package", 4 | "description": "Create a block-based theme", 5 | "keywords": [ 6 | "WordPress", 7 | "block" 8 | ], 9 | "homepage": "https://github.com/WordPress/create-block-theme", 10 | "license": "GPL-2.0-or-later", 11 | "authors": [ 12 | { 13 | "name": "Contributors", 14 | "homepage": "https://github.com/WordPress/create-block-theme/contributors.md" 15 | } 16 | ], 17 | "config": { 18 | "process-timeout": 0, 19 | "platform": { 20 | "php": "7.4" 21 | }, 22 | "allow-plugins": { 23 | "dealerdirect/phpcodesniffer-composer-installer": true, 24 | "composer/installers": true 25 | } 26 | }, 27 | "require-dev": { 28 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7", 29 | "squizlabs/php_codesniffer": "^3.5", 30 | "phpcompatibility/phpcompatibility-wp": "^2.1.3", 31 | "wp-coding-standards/wpcs": "^2.2", 32 | "sirbrillig/phpcs-variable-analysis": "^2.8", 33 | "spatie/phpunit-watcher": "^1.23", 34 | "yoast/phpunit-polyfills": "^1.1", 35 | "sempro/phpunit-pretty-print": "^1.4" 36 | }, 37 | "require": { 38 | "composer/installers": "~1.0" 39 | }, 40 | "scripts": { 41 | "format": "phpcbf --standard=phpcs.xml.dist --report-summary --report-source", 42 | "lint": "phpcs --standard=phpcs.xml.dist", 43 | "test": "phpunit", 44 | "test:watch": "phpunit-watcher watch < /dev/tty" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /create-block-theme.php: -------------------------------------------------------------------------------- 1 | run(); 44 | 45 | } 46 | cbt_run_create_block_theme(); 47 | -------------------------------------------------------------------------------- /includes/class-create-block-theme-admin-landing.php: -------------------------------------------------------------------------------- 1 | plugins_url( 'create-block-theme/assets/' ), 43 | 'editor_url' => admin_url( 'site-editor.php?canvas=edit' ), 44 | ) 45 | ); 46 | 47 | // Enable localization in the app. 48 | wp_set_script_translations( 'create-block-theme-app', 'create-block-theme' ); 49 | 50 | echo '
'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /includes/class-create-block-theme-editor-tools.php: -------------------------------------------------------------------------------- 1 | actions = array(); 34 | $this->filters = array(); 35 | 36 | } 37 | 38 | /** 39 | * Add a new action to the collection to be registered with WordPress. 40 | * 41 | * @since 1.0.0 42 | * @param string $hook The name of the WordPress action that is being registered. 43 | * @param object $component A reference to the instance of the object on which the action is defined. 44 | * @param string $callback The name of the function definition on the $component. 45 | * @param int $priority Optional. The priority at which the function should be fired. Default is 10. 46 | * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1. 47 | */ 48 | public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) { 49 | $this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args ); 50 | } 51 | 52 | /** 53 | * Add a new filter to the collection to be registered with WordPress. 54 | * 55 | * @since 1.0.0 56 | * @param string $hook The name of the WordPress filter that is being registered. 57 | * @param object $component A reference to the instance of the object on which the filter is defined. 58 | * @param string $callback The name of the function definition on the $component. 59 | * @param int $priority Optional. The priority at which the function should be fired. Default is 10. 60 | * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1 61 | */ 62 | public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) { 63 | $this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args ); 64 | } 65 | 66 | /** 67 | * A utility function that is used to register the actions and hooks into a single 68 | * collection. 69 | * 70 | * @since 1.0.0 71 | * @access private 72 | * @param array $hooks The collection of hooks that is being registered (that is, actions or filters). 73 | * @param string $hook The name of the WordPress filter that is being registered. 74 | * @param object $component A reference to the instance of the object on which the filter is defined. 75 | * @param string $callback The name of the function definition on the $component. 76 | * @param int $priority The priority at which the function should be fired. 77 | * @param int $accepted_args The number of arguments that should be passed to the $callback. 78 | * @return array The collection of actions and filters registered with WordPress. 79 | */ 80 | private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) { 81 | 82 | $hooks[] = array( 83 | 'hook' => $hook, 84 | 'component' => $component, 85 | 'callback' => $callback, 86 | 'priority' => $priority, 87 | 'accepted_args' => $accepted_args, 88 | ); 89 | 90 | return $hooks; 91 | 92 | } 93 | 94 | /** 95 | * Register the filters and actions with WordPress. 96 | * 97 | * @since 1.0.0 98 | */ 99 | public function run() { 100 | 101 | foreach ( $this->filters as $hook ) { 102 | add_filter( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] ); 103 | } 104 | 105 | foreach ( $this->actions as $hook ) { 106 | add_action( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] ); 107 | } 108 | 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /includes/class-create-block-theme.php: -------------------------------------------------------------------------------- 1 | load_dependencies(); 22 | $this->define_admin_hooks(); 23 | 24 | } 25 | 26 | /** 27 | * Load the required dependencies for this plugin. 28 | * 29 | * @since 0.0.2 30 | * @access private 31 | */ 32 | private function load_dependencies() { 33 | 34 | /** 35 | * The class responsible for orchestrating the actions and filters of the 36 | * core plugin. 37 | */ 38 | require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-create-block-theme-loader.php'; 39 | 40 | /** 41 | * The class responsible for defining all actions that occur in the admin area. 42 | */ 43 | require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-create-block-theme-api.php'; 44 | require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-create-block-theme-editor-tools.php'; 45 | require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-create-block-theme-admin-landing.php'; 46 | 47 | $this->loader = new CBT_Plugin_Loader(); 48 | 49 | } 50 | 51 | /** 52 | * Register all of the hooks related to the admin area functionality 53 | * of the plugin. 54 | * 55 | * @since 0.0.2 56 | * @access private 57 | */ 58 | private function define_admin_hooks() { 59 | $plugin_api = new CBT_Theme_API(); 60 | $editor_tools = new CBT_Editor_Tools(); 61 | $admin_landing = new CBT_Admin_Landing(); 62 | } 63 | 64 | /** 65 | * Run the loader to execute all of the hooks with WordPress. 66 | * 67 | * @since 0.0.2 68 | */ 69 | public function run() { 70 | $this->loader->run(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /includes/create-theme/cbt-zip-archive.php: -------------------------------------------------------------------------------- 1 | theme_folder = $folder; 15 | } 16 | 17 | function addFromStringToTheme( $name, $content ) { 18 | $name = $this->theme_folder . '/' . $name; 19 | return parent::addFromString( $name, $content ); 20 | } 21 | 22 | function addFileToTheme( $filepath, $entryname ) { 23 | $entryname = $this->theme_folder . '/' . $entryname; 24 | return parent::addFile( $filepath, $entryname ); 25 | } 26 | 27 | function addThemeDir( $dirname ) { 28 | $dirname = $this->theme_folder . '/' . $dirname; 29 | return parent::addEmptyDir( $dirname ); 30 | } 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /includes/create-theme/resolver_additions.php: -------------------------------------------------------------------------------- 1 | parent() ) { 32 | // Get parent theme.json. 33 | $parent_theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json', true ) ); 34 | $parent_theme_json_data = static::translate( $parent_theme_json_data, $current_theme->parent()->get( 'TextDomain' ) ); 35 | 36 | // Get the schema from the parent JSON. 37 | $schema = $parent_theme_json_data['$schema']; 38 | if ( array_key_exists( 'schema', $parent_theme_json_data ) ) { 39 | $schema = $parent_theme_json_data['$schema']; 40 | } 41 | 42 | if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) { 43 | $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data ); 44 | } else { 45 | $parent_theme = new WP_Theme_JSON( $parent_theme_json_data ); 46 | } 47 | $theme->merge( $parent_theme ); 48 | } 49 | 50 | if ( 'all' === $content || 'current' === $content ) { 51 | $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) ); 52 | $theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) ); 53 | 54 | // Get the schema from the parent JSON. 55 | if ( array_key_exists( 'schema', $theme_json_data ) ) { 56 | $schema = $theme_json_data['$schema']; 57 | } 58 | 59 | if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) { 60 | $theme_theme = new WP_Theme_JSON_Gutenberg( $theme_json_data ); 61 | } else { 62 | $theme_theme = new WP_Theme_JSON( $theme_json_data ); 63 | } 64 | $theme->merge( $theme_theme ); 65 | } 66 | 67 | // Merge the User Data 68 | $theme->merge( static::get_user_data() ); 69 | 70 | // Merge the extra theme data received as a parameter 71 | if ( ! empty( $extra_theme_data ) ) { 72 | if ( class_exists( 'WP_Theme_JSON_Gutenberg' ) ) { 73 | $extra_data = new WP_Theme_JSON_Gutenberg( $extra_theme_data ); 74 | } else { 75 | $extra_data = new WP_Theme_JSON( $extra_theme_data ); 76 | } 77 | $theme->merge( $extra_data ); 78 | } 79 | 80 | $data = $theme->get_data(); 81 | 82 | // Add the schema. 83 | if ( empty( $schema ) ) { 84 | global $wp_version; 85 | $theme_json_version = 'wp/' . substr( $wp_version, 0, 3 ); 86 | if ( defined( 'IS_GUTENBERG_PLUGIN' ) ) { 87 | $theme_json_version = 'trunk'; 88 | } 89 | $schema = 'https://schemas.wp.org/' . $theme_json_version . '/theme.json'; 90 | } 91 | $data['$schema'] = $schema; 92 | return static::stringify( $data ); 93 | } 94 | 95 | /** 96 | * Get the user data. 97 | * 98 | * This is a copy of the parent function with the addition of the Gutenberg resolver. 99 | * 100 | * @return array 101 | */ 102 | public static function get_user_data() { 103 | // Determine the correct method to retrieve user data 104 | return class_exists( 'WP_Theme_JSON_Resolver_Gutenberg' ) 105 | ? WP_Theme_JSON_Resolver_Gutenberg::get_user_data() 106 | : parent::get_user_data(); 107 | } 108 | 109 | /** 110 | * Stringify the array data. 111 | * 112 | * $data is an array of data to be converted to a JSON string. 113 | * @return string JSON string. 114 | */ 115 | public static function stringify( $data ) { 116 | $data = wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); 117 | // Convert spaces to tabs 118 | return preg_replace( '~(?:^|\G)\h{4}~m', "\t", $data ); 119 | } 120 | 121 | public static function get_theme_file_contents() { 122 | $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) ); 123 | return $theme_json_data; 124 | } 125 | 126 | public static function write_theme_file_contents( $theme_json_data ) { 127 | $theme_json = wp_json_encode( $theme_json_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); 128 | file_put_contents( static::get_file_path_from_theme( 'theme.json' ), $theme_json ); 129 | static::clean_cached_data(); 130 | } 131 | 132 | public static function write_user_settings( $user_settings ) { 133 | $global_styles_id = static::get_user_global_styles_post_id(); 134 | $request = new WP_REST_Request( 'POST', '/wp/v2/global-styles/' . $global_styles_id ); 135 | $request->set_param( 'settings', $user_settings ); 136 | rest_do_request( $request ); 137 | static::clean_cached_data(); 138 | } 139 | 140 | public static function clean_cached_data() { 141 | parent::clean_cached_data(); 142 | 143 | if ( class_exists( 'WP_Theme_JSON_Resolver_Gutenberg' ) ) { 144 | WP_Theme_JSON_Resolver_Gutenberg::clean_cached_data(); 145 | } 146 | 147 | //TODO: Clearing the cache should clear this too. 148 | // Does this clear the Gutenberg equivalent? 149 | static::$theme_json_file_cache = array(); 150 | } 151 | } 152 | } 153 | 154 | add_action( 'plugins_loaded', 'cbt_augment_resolver_with_utilities' ); 155 | -------------------------------------------------------------------------------- /includes/create-theme/theme-create.php: -------------------------------------------------------------------------------- 1 | 'image/png', 7 | ); 8 | 9 | public static function clone_current_theme( $theme ) { 10 | // Default values for cloned themes 11 | $theme['is_cloned_theme'] = true; 12 | $theme['version'] = '1.0'; 13 | $theme['tags_custom'] = implode( ', ', wp_get_theme()->get( 'Tags' ) ); 14 | 15 | // Create theme directory. 16 | $new_theme_path = get_theme_root() . DIRECTORY_SEPARATOR . $theme['slug']; 17 | 18 | if ( file_exists( $new_theme_path ) ) { 19 | return new WP_Error( 'theme_already_exists', __( 'Theme already exists.', 'create-block-theme' ) ); 20 | } 21 | 22 | wp_mkdir_p( $new_theme_path ); 23 | 24 | // Persist font settings for cloned theme. 25 | CBT_Theme_Fonts::persist_font_settings(); 26 | 27 | // Copy theme files. 28 | $template_options = array( 29 | 'localizeText' => false, 30 | 'removeNavRefs' => false, 31 | 'localizeImages' => false, 32 | ); 33 | CBT_Theme_Utils::clone_theme_to_folder( $new_theme_path, $theme['slug'], $theme['name'] ); 34 | CBT_Theme_Templates::add_templates_to_local( 'all', $new_theme_path, $theme['slug'], $template_options ); 35 | file_put_contents( path_join( $new_theme_path, 'theme.json' ), CBT_Theme_JSON_Resolver::export_theme_data( 'all' ) ); 36 | file_put_contents( path_join( $new_theme_path, 'readme.txt' ), CBT_Theme_Readme::create( $theme ) ); 37 | file_put_contents( path_join( $new_theme_path, 'style.css' ), CBT_Theme_Styles::update_style_css( file_get_contents( path_join( $new_theme_path, 'style.css' ) ), $theme ) ); 38 | 39 | switch_theme( $theme['slug'] ); 40 | } 41 | 42 | public static function create_child_theme( $theme, $screenshot ) { 43 | 44 | // Create theme directory. 45 | $new_theme_path = get_theme_root() . DIRECTORY_SEPARATOR . $theme['slug']; 46 | 47 | if ( file_exists( $new_theme_path ) ) { 48 | return new WP_Error( 'theme_already_exists', __( 'Theme already exists.', 'create-block-theme' ) ); 49 | } 50 | 51 | wp_mkdir_p( $new_theme_path ); 52 | 53 | // Add readme.txt. 54 | file_put_contents( 55 | $new_theme_path . DIRECTORY_SEPARATOR . 'readme.txt', 56 | CBT_Theme_Readme::create( $theme ) 57 | ); 58 | 59 | // Add style.css. 60 | $theme['template'] = wp_get_theme()->get( 'TextDomain' ); 61 | $css_contents = CBT_Theme_Styles::build_style_css( $theme ); 62 | file_put_contents( 63 | $new_theme_path . DIRECTORY_SEPARATOR . 'style.css', 64 | $css_contents 65 | ); 66 | 67 | // Add theme.json 68 | CBT_Theme_Templates::add_templates_to_local( 'user', $new_theme_path, $theme['slug'] ); 69 | file_put_contents( $new_theme_path . DIRECTORY_SEPARATOR . 'theme.json', CBT_Theme_JSON_Resolver::export_theme_data( 'variation' ) ); 70 | 71 | // Add Screenshot 72 | if ( static::is_valid_screenshot( $screenshot ) ) { 73 | file_put_contents( 74 | $new_theme_path . DIRECTORY_SEPARATOR . 'screenshot.png', 75 | file_get_contents( $screenshot['tmp_name'] ) 76 | ); 77 | } else { 78 | $source = plugin_dir_path( __DIR__ ) . '../assets/boilerplate/screenshot.png'; 79 | copy( $source, $new_theme_path . DIRECTORY_SEPARATOR . 'screenshot.png' ); 80 | } 81 | 82 | switch_theme( $theme['slug'] ); 83 | } 84 | 85 | public static function create_blank_theme( $theme, $screenshot ) { 86 | 87 | // Create theme directory. 88 | $source = plugin_dir_path( __DIR__ ) . '../assets/boilerplate'; 89 | $blank_theme_path = get_theme_root() . DIRECTORY_SEPARATOR . $theme['slug']; 90 | 91 | if ( file_exists( $blank_theme_path ) ) { 92 | return new WP_Error( 'theme_already_exists', __( 'Theme already exists.', 'create-block-theme' ) ); 93 | } 94 | 95 | wp_mkdir_p( $blank_theme_path ); 96 | 97 | // Add readme.txt. 98 | file_put_contents( 99 | $blank_theme_path . DIRECTORY_SEPARATOR . 'readme.txt', 100 | CBT_Theme_Readme::create( $theme ) 101 | ); 102 | 103 | // Add new metadata. 104 | $css_contents = CBT_Theme_Styles::build_style_css( $theme ); 105 | 106 | // Add style.css. 107 | file_put_contents( 108 | $blank_theme_path . DIRECTORY_SEPARATOR . 'style.css', 109 | $css_contents 110 | ); 111 | 112 | $iterator = new \RecursiveIteratorIterator( 113 | new \RecursiveDirectoryIterator( $source, \RecursiveDirectoryIterator::SKIP_DOTS ), 114 | \RecursiveIteratorIterator::SELF_FIRST 115 | ); 116 | 117 | foreach ( 118 | $iterator as $item 119 | ) { 120 | if ( $item->isDir() ) { 121 | wp_mkdir_p( $blank_theme_path . DIRECTORY_SEPARATOR . $iterator->getSubPathname() ); 122 | } else { 123 | copy( $item, $blank_theme_path . DIRECTORY_SEPARATOR . $iterator->getSubPathname() ); 124 | } 125 | } 126 | 127 | // Overwrite default screenshot if one is provided. 128 | if ( static::is_valid_screenshot( $screenshot ) ) { 129 | file_put_contents( 130 | $blank_theme_path . DIRECTORY_SEPARATOR . 'screenshot.png', 131 | file_get_contents( $screenshot['tmp_name'] ) 132 | ); 133 | } 134 | 135 | if ( ! defined( 'IS_GUTENBERG_PLUGIN' ) ) { 136 | global $wp_version; 137 | $theme_json_version = 'wp/' . substr( $wp_version, 0, 3 ); 138 | $schema = '"$schema": "https://schemas.wp.org/' . $theme_json_version . '/theme.json"'; 139 | $theme_json_path = $blank_theme_path . DIRECTORY_SEPARATOR . 'theme.json'; 140 | $theme_json_string = file_get_contents( $theme_json_path ); 141 | $theme_json_string = str_replace( '"$schema": "https://schemas.wp.org/trunk/theme.json"', $schema, $theme_json_string ); 142 | file_put_contents( $theme_json_path, $theme_json_string ); 143 | } 144 | 145 | switch_theme( $theme['slug'] ); 146 | } 147 | 148 | private static function is_valid_screenshot( $file ) { 149 | if ( ! $file ) { 150 | return 0; 151 | } 152 | $filetype = wp_check_filetype( $file['name'], self::ALLOWED_SCREENSHOT_TYPES ); 153 | if ( is_uploaded_file( $file['tmp_name'] ) && in_array( $filetype['type'], self::ALLOWED_SCREENSHOT_TYPES, true ) && $file['size'] < 2097152 ) { 154 | return 1; 155 | } 156 | return 0; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /includes/create-theme/theme-json.php: -------------------------------------------------------------------------------- 1 | merge( $user_data ); 26 | $variation = $theme_json->get_data(); 27 | $variation['title'] = $theme['name']; 28 | 29 | if ( 30 | $save_fonts && 31 | isset( $variation['settings']['typography']['fontFamilies'] ) 32 | ) { 33 | $font_families = $variation['settings']['typography']['fontFamilies']; 34 | // Copy the font assets to the theme assets folder. 35 | $copied_font_families = CBT_Theme_Fonts::copy_font_assets_to_theme( $font_families ); 36 | // Update the the variation theme json with the font families with the new paths. 37 | $variation['settings']['typography']['fontFamilies'] = $copied_font_families; 38 | } 39 | 40 | file_put_contents( 41 | $variation_path . $theme['slug'] . '.json', 42 | CBT_Theme_JSON_Resolver::stringify( $variation ) 43 | ); 44 | 45 | return $variation; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /includes/create-theme/theme-locale.php: -------------------------------------------------------------------------------- 1 | process_tokens(); 36 | $text = $p->get_text(); 37 | $tokens = $p->get_tokens(); 38 | $translators_note = $p->get_translators_note(); 39 | 40 | if ( ! empty( $tokens ) ) { 41 | $php_tag = 'get( 'TextDomain' ) . "' ), " . implode( 44 | ', ', 45 | array_map( 46 | function( $token ) { 47 | return "'$token'"; 48 | }, 49 | $tokens 50 | ) 51 | ) . ' ); ?>'; 52 | return $php_tag; 53 | } 54 | 55 | return "get( 'TextDomain' ) . "');?>"; 56 | } 57 | 58 | /** 59 | * Escape an html element attribute for localization. 60 | * 61 | * @param string $string The string to escape. 62 | * @return string The escaped string. 63 | */ 64 | private static function escape_attribute( $string ) { 65 | // Avoid escaping if the text is not a string. 66 | if ( ! is_string( $string ) ) { 67 | return $string; 68 | } 69 | 70 | // Check if string is empty. 71 | if ( '' === $string ) { 72 | return $string; 73 | } 74 | 75 | // Check if the text is already escaped. 76 | if ( str_starts_with( $string, 'get( 'TextDomain' ) . "');?>"; 82 | } 83 | 84 | /** 85 | * Get a replacement pattern for escaping the text from the html content of a block. 86 | * 87 | * @param string $block_name The block name. 88 | * @return array|null The regex patterns to match the content that needs to be escaped. 89 | * Returns null if the block is not supported. 90 | * Returns an array of regex patterns if the block has html elements that need to be escaped. 91 | */ 92 | private static function get_text_replacement_patterns_for_html( $block_name ) { 93 | switch ( $block_name ) { 94 | case 'core/paragraph': 95 | return array( '/(]*>)(.*?)(<\/p>)/' ); 96 | case 'core/heading': 97 | return array( '/(]*>)(.*?)(<\/h[^>]*>)/' ); 98 | case 'core/list-item': 99 | return array( '/(]*>)(.*?)(<\/li>)/' ); 100 | case 'core/verse': 101 | return array( '/(]*>)(.*?)(<\/pre>)/' ); 102 | case 'core/button': 103 | return array( '/(]*>)(.*?)(<\/a>)/' ); 104 | case 'core/quote': 105 | case 'core/pullquote': 106 | return array( 107 | '/(]*>)(.*?)(<\/p>)/', 108 | '/(]*>)(.*?)(<\/cite>)/', 109 | ); 110 | case 'core/table': 111 | return array( 112 | '/(]*>)(.*?)(<\/td>)/', 113 | '/(]*>)(.*?)(<\/th>)/', 114 | '/(]*>)(.*?)(<\/figcaption>)/', 115 | ); 116 | case 'core/video': 117 | return array( '/(]*>)(.*?)(<\/figcaption>)/' ); 118 | case 'core/image': 119 | return array( 120 | '/(]*>)(.*?)(<\/figcaption>)/', 121 | '/(alt=")(.*?)(")/', 122 | ); 123 | case 'core/cover': 124 | case 'core/media-text': 125 | return array( '/(alt=")(.*?)(")/' ); 126 | default: 127 | return null; 128 | } 129 | } 130 | 131 | /* 132 | * Localize text in text blocks. 133 | * 134 | * @param array $blocks The blocks to localize. 135 | * @return array The localized blocks. 136 | */ 137 | public static function escape_text_content_of_blocks( $blocks ) { 138 | foreach ( $blocks as &$block ) { 139 | 140 | // Recursively escape the inner blocks. 141 | if ( ! empty( $block['innerBlocks'] ) ) { 142 | $block['innerBlocks'] = self::escape_text_content_of_blocks( $block['innerBlocks'] ); 143 | } 144 | 145 | /* 146 | * Set the pattern based on the block type. 147 | * The pattern is used to match the content that needs to be escaped. 148 | * Patterns are defined in the get_text_replacement_patterns_for_html method. 149 | */ 150 | $patterns = self::get_text_replacement_patterns_for_html( $block['blockName'] ); 151 | 152 | // If the block does not have any patterns leave the block as is and continue to the next block. 153 | if ( ! $patterns ) { 154 | continue; 155 | } 156 | 157 | // Builds the replacement callback function based on the block type. 158 | switch ( $block['blockName'] ) { 159 | case 'core/paragraph': 160 | case 'core/heading': 161 | case 'core/list-item': 162 | case 'core/verse': 163 | case 'core/button': 164 | case 'core/quote': 165 | case 'core/pullquote': 166 | case 'core/table': 167 | case 'core/video': 168 | case 'core/image': 169 | case 'core/cover': 170 | case 'core/media-text': 171 | $replace_content_callback = function ( $content, $pattern ) { 172 | if ( empty( $content ) ) { 173 | return; 174 | } 175 | return preg_replace_callback( 176 | $pattern, 177 | function( $matches ) { 178 | // If the pattern is for attribute like alt="". 179 | if ( str_ends_with( $matches[1], '="' ) ) { 180 | return $matches[1] . self::escape_attribute( $matches[2] ) . $matches[3]; 181 | } 182 | return $matches[1] . self::escape_text_content( $matches[2] ) . $matches[3]; 183 | }, 184 | $content 185 | ); 186 | }; 187 | break; 188 | default: 189 | $replace_content_callback = null; 190 | break; 191 | } 192 | 193 | // Apply the replacement patterns to the block content. 194 | foreach ( $patterns as $pattern ) { 195 | if ( 196 | ! empty( $block['innerContent'] ) && 197 | is_callable( $replace_content_callback ) 198 | ) { 199 | $block['innerContent'] = is_array( $block['innerContent'] ) 200 | ? array_map( 201 | function( $content ) use ( $replace_content_callback, $pattern ) { 202 | return $replace_content_callback( $content, $pattern ); 203 | }, 204 | $block['innerContent'] 205 | ) 206 | : $replace_content_callback( $block['innerContent'], $pattern ); 207 | } 208 | } 209 | } 210 | return $blocks; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /includes/create-theme/theme-media.php: -------------------------------------------------------------------------------- 1 | content ); 28 | $blocks = _flatten_blocks( $template_blocks ); 29 | 30 | $media = array(); 31 | 32 | foreach ( $blocks as $block ) { 33 | // Gets the absolute URLs of img in these blocks 34 | if ( 'core/image' === $block['blockName'] || 35 | 'core/video' === $block['blockName'] || 36 | 'core/cover' === $block['blockName'] || 37 | 'core/media-text' === $block['blockName'] 38 | ) { 39 | $html = new WP_HTML_Tag_Processor( $block['innerHTML'] ); 40 | while ( $html->next_tag( 'img' ) ) { 41 | $url = $html->get_attribute( 'src' ); 42 | if ( CBT_Theme_Utils::is_absolute_url( $url ) ) { 43 | $media[] = $url; 44 | } 45 | } 46 | $html = new WP_HTML_Tag_Processor( $html->__toString() ); 47 | while ( $html->next_tag( 'video' ) ) { 48 | $url = $html->get_attribute( 'src' ); 49 | if ( CBT_Theme_Utils::is_absolute_url( $url ) ) { 50 | $media[] = $url; 51 | } 52 | $poster_url = $html->get_attribute( 'poster' ); 53 | if ( CBT_Theme_Utils::is_absolute_url( $poster_url ) ) { 54 | $media[] = $poster_url; 55 | } 56 | } 57 | } 58 | 59 | // Gets the absolute URLs of background images in these blocks 60 | if ( 'core/cover' === $block['blockName'] ) { 61 | $html = new WP_HTML_Tag_Processor( $block['innerHTML'] ); 62 | while ( $html->next_tag( 'div' ) ) { 63 | $style = $html->get_attribute( 'style' ); 64 | if ( $style ) { 65 | $matches = array(); 66 | preg_match( '/background-image: url\((.*)\)/', $style, $matches ); 67 | if ( isset( $matches[1] ) ) { 68 | $url = $matches[1]; 69 | if ( CBT_Theme_Utils::is_absolute_url( $url ) ) { 70 | $media[] = $url; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | // Gets the absolute URLs of background images in these blocks 78 | if ( 'core/group' === $block['blockName'] ) { 79 | if ( isset( $block['attrs']['style']['background']['backgroundImage']['url'] ) && CBT_Theme_Utils::is_absolute_url( $block['attrs']['style']['background']['backgroundImage']['url'] ) ) { 80 | $media[] = $block['attrs']['style']['background']['backgroundImage']['url']; 81 | } 82 | } 83 | } 84 | 85 | return $media; 86 | } 87 | 88 | /** 89 | * Create a relative URL based on the absolute URL of a media file 90 | * 91 | * @param string $absolute_url 92 | * @return string $relative_url 93 | */ 94 | public static function make_relative_media_url( $absolute_url ) { 95 | if ( ! empty( $absolute_url ) && CBT_Theme_Utils::is_absolute_url( $absolute_url ) ) { 96 | $folder_path = self::get_media_folder_path_from_url( $absolute_url ); 97 | if ( is_child_theme() ) { 98 | return '' . $folder_path . basename( $absolute_url ); 99 | } 100 | return '' . $folder_path . basename( $absolute_url ); 101 | } 102 | return $absolute_url; 103 | } 104 | 105 | /** 106 | * Add media files to the local theme 107 | */ 108 | public static function add_media_to_local( $media ) { 109 | 110 | foreach ( $media as $url ) { 111 | 112 | $download_file = download_url( $url ); 113 | 114 | if ( is_wp_error( $download_file ) ) { 115 | //we're going to try again with a new URL 116 | //see, we might be running this in a docker container 117 | //and if that's the case let's try again on port 80 118 | $parsed_url = parse_url( $url ); 119 | if ( 'localhost' === $parsed_url['host'] && '80' !== $parsed_url['port'] ) { 120 | $download_file = download_url( str_replace( 'localhost:' . $parsed_url['port'], 'localhost:80', $url ) ); 121 | } 122 | } 123 | 124 | // TODO: implement a warning if the file is missing 125 | if ( ! is_wp_error( $download_file ) ) { 126 | $media_path = get_stylesheet_directory() . DIRECTORY_SEPARATOR . self::get_media_folder_path_from_url( $url ); 127 | if ( ! is_dir( $media_path ) ) { 128 | wp_mkdir_p( $media_path ); 129 | } 130 | rename( $download_file, $media_path . basename( $url ) ); 131 | } 132 | } 133 | 134 | } 135 | 136 | 137 | /** 138 | * Replace the absolute URLs of media in a template with relative URLs 139 | */ 140 | public static function make_template_images_local( $template ) { 141 | 142 | $template->media = self::get_media_absolute_urls_from_template( $template ); 143 | 144 | // Replace the absolute URLs with relative URLs in the templates 145 | foreach ( $template->media as $media_url ) { 146 | $local_media_url = CBT_Theme_Media::make_relative_media_url( $media_url ); 147 | $template->content = str_replace( $media_url, $local_media_url, $template->content ); 148 | } 149 | 150 | return $template; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /includes/create-theme/theme-patterns.php: -------------------------------------------------------------------------------- 1 | get( 'TextDomain' ); 6 | $pattern_slug = $theme_slug . '/' . $template->slug; 7 | $pattern_content = <<slug} 11 | * Slug: {$pattern_slug} 12 | * Inserter: no 13 | */ 14 | ?> 15 | {$template->content} 16 | PHP; 17 | 18 | return array( 19 | 'slug' => $pattern_slug, 20 | 'content' => $pattern_content, 21 | ); 22 | } 23 | 24 | public static function pattern_from_wp_block( $pattern_post ) { 25 | $pattern = new stdClass(); 26 | $pattern->id = $pattern_post->ID; 27 | $pattern->title = $pattern_post->post_title; 28 | $pattern->name = sanitize_title_with_dashes( $pattern_post->post_title ); 29 | $pattern->slug = wp_get_theme()->get( 'TextDomain' ) . '/' . $pattern->name; 30 | $pattern_category_list = get_the_terms( $pattern->id, 'wp_pattern_category' ); 31 | $pattern->categories = ! empty( $pattern_category_list ) ? join( ', ', wp_list_pluck( $pattern_category_list, 'name' ) ) : ''; 32 | $pattern->sync_status = get_post_meta( $pattern->id, 'wp_pattern_sync_status', true ); 33 | $pattern->content = <<title} 37 | * Slug: {$pattern->slug} 38 | * Categories: {$pattern->categories} 39 | */ 40 | ?> 41 | {$pattern_post->post_content} 42 | PHP; 43 | 44 | return $pattern; 45 | } 46 | 47 | public static function escape_alt_for_pattern( $html ) { 48 | if ( empty( $html ) ) { 49 | return $html; 50 | } 51 | $html = new WP_HTML_Tag_Processor( $html ); 52 | while ( $html->next_tag( 'img' ) ) { 53 | $alt_attribute = $html->get_attribute( 'alt' ); 54 | if ( ! empty( $alt_attribute ) ) { 55 | $html->set_attribute( 'alt', self::escape_text_for_pattern( $alt_attribute ) ); 56 | } 57 | } 58 | return $html->__toString(); 59 | } 60 | 61 | public static function escape_text_for_pattern( $text ) { 62 | if ( $text && trim( $text ) !== '' ) { 63 | $escaped_text = addslashes( $text ); 64 | return "get( 'Name' ) . "');?>"; 65 | } 66 | } 67 | 68 | public static function create_pattern_link( $attributes ) { 69 | $block_attributes = array_filter( $attributes ); 70 | $attributes_json = json_encode( $block_attributes, JSON_UNESCAPED_SLASHES ); 71 | return ''; 72 | } 73 | 74 | public static function replace_local_pattern_references( $pattern ) { 75 | // Find any references to pattern in templates 76 | $templates_to_update = array(); 77 | $args = array( 78 | 'post_type' => array( 'wp_template', 'wp_template_part' ), 79 | 'posts_per_page' => -1, 80 | 's' => 'wp:block {"ref":' . $pattern->id . '}', 81 | ); 82 | $find_pattern_refs = new WP_Query( $args ); 83 | if ( $find_pattern_refs->have_posts() ) { 84 | foreach ( $find_pattern_refs->posts as $post ) { 85 | $slug = $post->post_name; 86 | array_push( $templates_to_update, $slug ); 87 | } 88 | } 89 | $templates_to_update = array_unique( $templates_to_update ); 90 | 91 | // Only update templates that reference the pattern 92 | CBT_Theme_Templates::add_templates_to_local( 'all', null, null, $options, $templates_to_update ); 93 | 94 | // List all template and pattern files in the theme 95 | $base_dir = get_stylesheet_directory(); 96 | $patterns = glob( $base_dir . DIRECTORY_SEPARATOR . 'patterns' . DIRECTORY_SEPARATOR . '*.php' ); 97 | $templates = glob( $base_dir . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . '*.html' ); 98 | $template_parts = glob( $base_dir . DIRECTORY_SEPARATOR . 'template-parts' . DIRECTORY_SEPARATOR . '*.html' ); 99 | 100 | // Replace references to the local patterns in the theme 101 | foreach ( array_merge( $patterns, $templates, $template_parts ) as $file ) { 102 | $file_content = file_get_contents( $file ); 103 | $file_content = str_replace( 'wp:block {"ref":' . $pattern->id . '}', 'wp:pattern {"slug":"' . $pattern->slug . '"}', $file_content ); 104 | file_put_contents( $file, $file_content ); 105 | } 106 | 107 | CBT_Theme_Templates::clear_user_templates_customizations(); 108 | CBT_Theme_Templates::clear_user_template_parts_customizations(); 109 | } 110 | 111 | public static function prepare_pattern_for_export( $pattern, $options = null ) { 112 | if ( ! $options ) { 113 | $options = array( 114 | 'localizeText' => false, 115 | 'removeNavRefs' => true, 116 | 'localizeImages' => true, 117 | ); 118 | } 119 | 120 | $pattern = CBT_Theme_Templates::eliminate_environment_specific_content( $pattern, $options ); 121 | 122 | if ( array_key_exists( 'localizeText', $options ) && $options['localizeText'] ) { 123 | $pattern = CBT_Theme_Templates::escape_text_in_template( $pattern ); 124 | } 125 | 126 | if ( array_key_exists( 'localizeImages', $options ) && $options['localizeImages'] ) { 127 | $pattern = CBT_Theme_Media::make_template_images_local( $pattern ); 128 | 129 | // Write the media assets if there are any 130 | if ( $pattern->media ) { 131 | CBT_Theme_Media::add_media_to_local( $pattern->media ); 132 | } 133 | } 134 | 135 | return $pattern; 136 | } 137 | 138 | /** 139 | * Copy the local patterns as well as any media to the theme filesystem. 140 | */ 141 | public static function add_patterns_to_theme( $options = null ) { 142 | $base_dir = get_stylesheet_directory(); 143 | $patterns_dir = $base_dir . DIRECTORY_SEPARATOR . 'patterns'; 144 | 145 | $pattern_query = new WP_Query( 146 | array( 147 | 'post_type' => 'wp_block', 148 | 'posts_per_page' => -1, 149 | ) 150 | ); 151 | 152 | if ( $pattern_query->have_posts() ) { 153 | // If there is no patterns folder, create it. 154 | if ( ! is_dir( $patterns_dir ) ) { 155 | wp_mkdir_p( $patterns_dir ); 156 | } 157 | 158 | foreach ( $pattern_query->posts as $pattern ) { 159 | $pattern = self::pattern_from_wp_block( $pattern ); 160 | $pattern = self::prepare_pattern_for_export( $pattern, $options ); 161 | $pattern_exists = false; 162 | 163 | // Check pattern is synced before adding to theme. 164 | if ( 'unsynced' !== $pattern->sync_status ) { 165 | // Check pattern name doesn't already exist before creating the file. 166 | $existing_patterns = glob( $patterns_dir . DIRECTORY_SEPARATOR . '*.php' ); 167 | foreach ( $existing_patterns as $existing_pattern ) { 168 | if ( strpos( $existing_pattern, $pattern->name . '.php' ) !== false ) { 169 | $pattern_exists = true; 170 | } 171 | } 172 | 173 | if ( $pattern_exists ) { 174 | return new WP_Error( 175 | 'pattern_already_exists', 176 | sprintf( 177 | /* Translators: Pattern name. */ 178 | __( 179 | 'A pattern with this name already exists: "%s".', 180 | 'create-block-theme' 181 | ), 182 | $pattern->name 183 | ) 184 | ); 185 | } 186 | 187 | // Create the pattern file. 188 | $pattern_file = $patterns_dir . $pattern->name . '.php'; 189 | file_put_contents( 190 | $patterns_dir . DIRECTORY_SEPARATOR . $pattern->name . '.php', 191 | $pattern->content 192 | ); 193 | 194 | self::replace_local_pattern_references( $pattern ); 195 | 196 | // Remove it from the database to ensure that these patterns are loaded from the theme. 197 | wp_delete_post( $pattern->id, true ); 198 | } 199 | } 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /includes/create-theme/theme-styles.php: -------------------------------------------------------------------------------- 1 | 'License', 16 | 'LicenseURI' => 'License URI', 17 | ) 18 | ); 19 | 20 | $current_theme = wp_get_theme(); 21 | $css_contents = trim( substr( $style_css, strpos( $style_css, '*/' ) + 2 ) ); 22 | $name = stripslashes( $theme['name'] ); 23 | $description = stripslashes( $theme['description'] ); 24 | $uri = $theme['uri']; 25 | $author = stripslashes( $theme['author'] ); 26 | $author_uri = $theme['author_uri']; 27 | $wp_version = CBT_Theme_Utils::get_current_wordpress_version(); 28 | $requires_wp = ( '' === $theme['requires_wp'] ) ? CBT_Theme_Utils::get_current_wordpress_version() : $theme['requires_wp']; 29 | $version = $theme['version']; 30 | $requires_php = $current_theme->get( 'RequiresPHP' ); 31 | $text_domain = $theme['slug']; 32 | $template = $current_theme->get( 'Template' ) ? "\n" . 'Template: ' . $current_theme->get( 'Template' ) : ''; 33 | $license = $style_data['License'] ? $style_data['License'] : 'GNU General Public License v2 or later'; 34 | $license_uri = $style_data['LicenseURI'] ? $style_data['LicenseURI'] : 'http://www.gnu.org/licenses/gpl-2.0.html'; 35 | $tags = CBT_Theme_Tags::theme_tags_list( $theme ); 36 | $css_contents = $css_contents ? "\n\n" . $css_contents : ''; 37 | $copyright = ''; 38 | preg_match( '/^\s*\n((?s).*?)\*\/\s*$/m', $style_css, $matches ); 39 | if ( isset( $matches[1] ) ) { 40 | $copyright = "\n" . $matches[1]; 41 | } 42 | 43 | return "/* 44 | Theme Name: {$name} 45 | Theme URI: {$uri} 46 | Author: {$author} 47 | Author URI: {$author_uri} 48 | Description: {$description} 49 | Requires at least: {$requires_wp} 50 | Tested up to: {$wp_version} 51 | Requires PHP: {$requires_php} 52 | Version: {$version} 53 | License: {$license} 54 | License URI: {$license_uri}{$template} 55 | Text Domain: {$text_domain} 56 | Tags: {$tags} 57 | {$copyright}*/{$css_contents} 58 | "; 59 | } 60 | 61 | /** 62 | * Build a style.css file for CHILD/GRANDCHILD themes. 63 | */ 64 | public static function build_style_css( $theme ) { 65 | $name = stripslashes( $theme['name'] ); 66 | $description = stripslashes( $theme['description'] ); 67 | $uri = $theme['uri']; 68 | $author = stripslashes( $theme['author'] ); 69 | $author_uri = $theme['author_uri']; 70 | $requires_wp = ( '' === $theme['requires_wp'] ) ? CBT_Theme_Utils::get_current_wordpress_version() : $theme['requires_wp']; 71 | $wp_version = CBT_Theme_Utils::get_current_wordpress_version(); 72 | $text_domain = sanitize_title( $name ); 73 | if ( isset( $theme['template'] ) ) { 74 | $template = $theme['template']; 75 | } 76 | $version = '1.0'; 77 | $tags = CBT_Theme_Tags::theme_tags_list( $theme ); 78 | 79 | if ( isset( $theme['version'] ) ) { 80 | $version = $theme['version']; 81 | } 82 | 83 | $style_css = "/* 84 | Theme Name: {$name} 85 | Theme URI: {$uri} 86 | Author: {$author} 87 | Author URI: {$author_uri} 88 | Description: {$description} 89 | Requires at least: {$requires_wp} 90 | Tested up to: {$wp_version} 91 | Requires PHP: 5.7 92 | Version: {$version} 93 | License: GNU General Public License v2 or later 94 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 95 | "; 96 | 97 | if ( ! empty( $template ) ) { 98 | $style_css .= "Template: {$template}\n"; 99 | } 100 | 101 | $style_css .= "Text Domain: {$text_domain} 102 | Tags: {$tags} 103 | */ 104 | 105 | "; 106 | 107 | return $style_css; 108 | } 109 | 110 | public static function clear_user_styles_customizations() { 111 | // Clear all values in the user theme.json 112 | $user_custom_post_type_id = WP_Theme_JSON_Resolver::get_user_global_styles_post_id(); 113 | $global_styles_controller = new WP_REST_Global_Styles_Controller(); 114 | $update_request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' ); 115 | $update_request->set_param( 'id', $user_custom_post_type_id ); 116 | $update_request->set_param( 'settings', array() ); 117 | $update_request->set_param( 'styles', array() ); 118 | $updated_global_styles = $global_styles_controller->update_item( $update_request ); 119 | delete_transient( 'global_styles' ); 120 | delete_transient( 'global_styles_' . get_stylesheet() ); 121 | delete_transient( 'gutenberg_global_styles' ); 122 | delete_transient( 'gutenberg_global_styles_' . get_stylesheet() ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /includes/create-theme/theme-tags.php: -------------------------------------------------------------------------------- 1 | 35 |
36 |
37 | 38 | 39 |

40 | ' . __( 'read more', 'create-block-theme' ) . '' 48 | ); 49 | ?> 50 |

51 | 52 |
53 | $tags ) { 60 | self::theme_tags_category( $category, $tags ); 61 | } 62 | } 63 | ?> 64 |
65 | 66 |

67 | 68 | 69 | 70 |

71 |

72 | 73 |

74 | 75 |
76 |
77 | 85 |
86 | 87 |   88 | 89 | 90 | 91 | 92 | $pretty_tag ) : ?> 93 | 94 | 95 |
96 | get( 'Tags' ); 139 | $default_tags = self::list_default_tags(); 140 | $merged_tags = array_unique( array_merge( $default_tags, $active_theme_tags ) ); 141 | 142 | return in_array( $tag, $merged_tags, true ); 143 | } 144 | 145 | /** 146 | * Build checkbox input for given theme tag. 147 | * 148 | * @param string $category 149 | * @param string $tag 150 | * @param string $pretty_tag 151 | * @return void 152 | */ 153 | protected static function tag_checkbox_input( $category, $tag, $pretty_tag ) { 154 | $class = ''; 155 | $checked = ''; 156 | 157 | if ( self::is_default_tag( $tag ) ) { 158 | $class = 'default-tag'; 159 | } 160 | 161 | if ( self::is_active_theme_tag( $tag ) ) { 162 | $checked = ' checked'; 163 | } 164 | ?> 165 |
166 | > 167 | 168 |
169 | p = new WP_HTML_Tag_Processor( $string ); 19 | } 20 | 21 | /** 22 | * Processes the HTML tags in the string and updates tokens, text, and translators' note. 23 | * 24 | * @param $p The string to process. 25 | * @return void 26 | */ 27 | public function process_tokens() { 28 | while ( $this->p->next_token() ) { 29 | $token_type = $this->p->get_token_type(); 30 | $token_name = strtolower( $this->p->get_token_name() ); 31 | $is_tag_closer = $this->p->is_tag_closer(); 32 | $has_self_closer = $this->p->has_self_closing_flag(); 33 | 34 | if ( '#tag' === $token_type ) { 35 | $this->increment++; 36 | $this->text .= '%' . $this->increment . '$s'; 37 | $token_label = $this->increment . '.'; 38 | 39 | if ( 1 !== $this->increment ) { 40 | $this->translators_note .= ', '; 41 | } 42 | 43 | if ( $is_tag_closer ) { 44 | $this->tokens[] = ""; 45 | $this->translators_note .= $token_label . " is the end of a '" . $token_name . "' HTML element"; 46 | } else { 47 | $token = '<' . $token_name; 48 | $attributes = $this->p->get_attribute_names_with_prefix( '' ); 49 | 50 | foreach ( $attributes as $attr_name ) { 51 | $attr_value = $this->p->get_attribute( $attr_name ); 52 | $token .= $this->process_attribute( $attr_name, $attr_value ); 53 | } 54 | 55 | $token .= '>'; 56 | $this->tokens[] = $token; 57 | 58 | if ( $has_self_closer || 'br' === $token_name ) { 59 | $this->translators_note .= $token_label . " is a '" . $token_name . "' HTML element"; 60 | } else { 61 | $this->translators_note .= $token_label . " is the start of a '" . $token_name . "' HTML element"; 62 | } 63 | } 64 | } else { 65 | // Escape text content. 66 | $temp_text = $this->p->get_modifiable_text(); 67 | 68 | // If the text contains a %, we need to escape it. 69 | if ( false !== strpos( $temp_text, '%' ) ) { 70 | $temp_text = str_replace( '%', '%%', $temp_text ); 71 | } 72 | 73 | $this->text .= $temp_text; 74 | } 75 | } 76 | 77 | if ( ! empty( $this->tokens ) ) { 78 | $this->translators_note .= ' */ '; 79 | } 80 | } 81 | 82 | /** 83 | * Processes individual tag attributes and escapes where necessary. 84 | * 85 | * @param string $attr_name The name of the attribute. 86 | * @param string $attr_value The value of the attribute. 87 | * @return string The processed attribute. 88 | */ 89 | private function process_attribute( $attr_name, $attr_value ) { 90 | $token_part = ''; 91 | if ( empty( $attr_value ) ) { 92 | $token_part .= ' ' . $attr_name; 93 | } elseif ( 'src' === $attr_name ) { 94 | CBT_Theme_Media::add_media_to_local( array( $attr_value ) ); 95 | $relative_src = CBT_Theme_Media::get_media_folder_path_from_url( $attr_value ) . basename( $attr_value ); 96 | $attr_value = "' . esc_url( get_stylesheet_directory_uri() ) . '{$relative_src}"; 97 | $token_part .= ' ' . $attr_name . '="' . $attr_value . '"'; 98 | } elseif ( 'href' === $attr_name ) { 99 | $attr_value = "' . esc_url( '$attr_value' ) . '"; 100 | $token_part .= ' ' . $attr_name . '="' . $attr_value . '"'; 101 | } else { 102 | $token_part .= ' ' . $attr_name . '="' . $attr_value . '"'; 103 | } 104 | 105 | return $token_part; 106 | } 107 | 108 | /** 109 | * Gets the processed text. 110 | * 111 | * @return string 112 | */ 113 | public function get_text() { 114 | return $this->text; 115 | } 116 | 117 | /** 118 | * Gets the processed tokens. 119 | * 120 | * @return array 121 | */ 122 | public function get_tokens() { 123 | return $this->tokens; 124 | } 125 | 126 | /** 127 | * Gets the generated translators' note. 128 | * 129 | * @return string 130 | */ 131 | public function get_translators_note() { 132 | return $this->translators_note; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /includes/create-theme/theme-utils.php: -------------------------------------------------------------------------------- 1 | get( 'TextDomain' ); 43 | $old_name = $theme->get( 'Name' ); 44 | 45 | // Get real path for our folder 46 | $theme_path = get_stylesheet_directory(); 47 | 48 | // Create recursive directory iterator 49 | /** @var SplFileInfo[] $files */ 50 | $files = new \RecursiveIteratorIterator( 51 | new \RecursiveDirectoryIterator( $theme_path, \RecursiveDirectoryIterator::SKIP_DOTS ), 52 | \RecursiveIteratorIterator::SELF_FIRST 53 | ); 54 | 55 | // Add all the files (except for templates) 56 | foreach ( $files as $name => $file ) { 57 | 58 | // Get real and relative path for current file 59 | $file_path = wp_normalize_path( $file ); 60 | $relative_path = substr( $file_path, strlen( $theme_path ) + 1 ); 61 | 62 | // Create Directories 63 | if ( $file->isDir() ) { 64 | wp_mkdir_p( $location . DIRECTORY_SEPARATOR . $files->getSubPathname() ); 65 | } 66 | 67 | // If the path is for templates/parts ignore it 68 | if ( 69 | strpos( $file_path, 'block-template-parts/' ) || 70 | strpos( $file_path, 'block-templates/' ) || 71 | strpos( $file_path, 'templates/' ) || 72 | strpos( $file_path, 'parts/' ) 73 | ) { 74 | continue; 75 | } 76 | 77 | // Replace only text files, skip png's and other stuff. 78 | $contents = file_get_contents( $file_path ); 79 | $valid_extensions = array( 'php', 'css', 'scss', 'js', 'txt', 'html' ); 80 | $valid_extensions_regex = implode( '|', $valid_extensions ); 81 | 82 | if ( preg_match( "/\.({$valid_extensions_regex})$/", $relative_path ) ) { 83 | // Replace namespace values if provided 84 | if ( $new_slug ) { 85 | $contents = self::replace_namespace( $contents, $old_slug, $new_slug, $old_name, $new_name ); 86 | } 87 | } 88 | 89 | // Add current file to target 90 | file_put_contents( $location . DIRECTORY_SEPARATOR . $relative_path, $contents ); 91 | } 92 | } 93 | 94 | public static function is_valid_screenshot( $file ) { 95 | 96 | $allowed_screenshot_types = array( 97 | 'png' => 'image/png', 98 | ); 99 | $filetype = wp_check_filetype( $file['name'], $allowed_screenshot_types ); 100 | if ( is_uploaded_file( $file['tmp_name'] ) && in_array( $filetype['type'], $allowed_screenshot_types, true ) && $file['size'] < 2097152 ) { 101 | return 1; 102 | } 103 | return 0; 104 | } 105 | 106 | public static function is_valid_screenshot_file( $file_path ) { 107 | return CBT_Theme_Utils::get_screenshot_file_extension( $file_path ) !== null; 108 | } 109 | 110 | public static function get_screenshot_file_extension( $file_path ) { 111 | $allowed_screenshot_types = array( 112 | 'png' => 'image/png', 113 | 'gif' => 'image/gif', 114 | 'jpg' => 'image/jpeg', 115 | 'jpeg' => 'image/jpeg', 116 | 'webp' => 'image/webp', 117 | 'avif' => 'image/avif', 118 | ); 119 | $filetype = wp_check_filetype( $file_path, $allowed_screenshot_types ); 120 | if ( in_array( $filetype['type'], $allowed_screenshot_types, true ) ) { 121 | return $filetype['ext']; 122 | } 123 | return null; 124 | } 125 | 126 | public static function copy_screenshot( $file_path ) { 127 | 128 | $new_screeenshot_id = attachment_url_to_postid( $file_path ); 129 | 130 | if ( ! $new_screeenshot_id ) { 131 | return new \WP_Error( 'screenshot_not_found', __( 'Screenshot not found', 'create-block-theme' ) ); 132 | } 133 | 134 | $new_screenshot_metadata = wp_get_attachment_metadata( $new_screeenshot_id ); 135 | $upload_dir = wp_get_upload_dir(); 136 | 137 | $new_screenshot_location = path_join( $upload_dir['basedir'], $new_screenshot_metadata['file'] ); 138 | 139 | $new_screenshot_filetype = CBT_Theme_Utils::get_screenshot_file_extension( $file_path ); 140 | $new_location = path_join( get_stylesheet_directory(), 'screenshot.' . $new_screenshot_filetype ); 141 | 142 | // copy and resize the image 143 | $image_editor = wp_get_image_editor( $new_screenshot_location ); 144 | $image_editor->resize( 1200, 900, true ); 145 | $image_editor->save( $new_location ); 146 | 147 | return true; 148 | } 149 | 150 | public static function replace_screenshot( $new_screenshot_path ) { 151 | if ( ! CBT_Theme_Utils::is_valid_screenshot_file( $new_screenshot_path ) ) { 152 | return new \WP_Error( 'invalid_screenshot', __( 'Invalid screenshot file', 'create-block-theme' ) ); 153 | } 154 | 155 | // Remove the old screenshot 156 | $old_screenshot = wp_get_theme()->get_screenshot( 'relative' ); 157 | if ( $old_screenshot ) { 158 | unlink( path_join( get_stylesheet_directory(), $old_screenshot ) ); 159 | } 160 | 161 | // Copy the new screenshot 162 | return CBT_Theme_Utils::copy_screenshot( $new_screenshot_path ); 163 | } 164 | 165 | /** 166 | * Get the current WordPress version. 167 | * 168 | * @return string The current WordPress in the format x.x (major.minor) 169 | * Example: 6.5 170 | */ 171 | public static function get_current_wordpress_version() { 172 | $wp_version = get_bloginfo( 'version' ); 173 | $wp_version_parts = explode( '.', $wp_version ); 174 | return $wp_version_parts[0] . '.' . $wp_version_parts[1]; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /includes/index.php: -------------------------------------------------------------------------------- 1 | =20.10.0", 19 | "npm": ">=10.2.3" 20 | }, 21 | "dependencies": { 22 | "@codemirror/lang-json": "^6.0.1", 23 | "@uiw/react-codemirror": "^4.23.1", 24 | "@wordpress/icons": "^10.7.0", 25 | "lib-font": "^2.4.3" 26 | }, 27 | "devDependencies": { 28 | "@actions/core": "^1.10.0", 29 | "@emotion/babel-plugin": "^11.12.0", 30 | "@wordpress/base-styles": "^5.7.0", 31 | "@wordpress/browserslist-config": "^6.7.0", 32 | "@wordpress/env": "^10.14.0", 33 | "@wordpress/eslint-plugin": "^21.0.0", 34 | "@wordpress/prettier-config": "^4.7.0", 35 | "@wordpress/scripts": "^29.0.0", 36 | "@wordpress/stylelint-config": "^22.7.0", 37 | "babel-plugin-inline-json-import": "^0.3.2", 38 | "eslint-plugin-unicorn": "^55.0.0", 39 | "husky": "^9.1.5", 40 | "lint-staged": "^15.2.10", 41 | "prettier": "npm:wp-prettier@3.0.3", 42 | "simple-git": "^3.26.0" 43 | }, 44 | "scripts": { 45 | "build": "wp-scripts build src/admin-landing-page.js src/plugin-sidebar.js", 46 | "format": "wp-scripts format", 47 | "lint:css": "wp-scripts lint-style", 48 | "lint:css:fix": "npm run lint:css -- --fix", 49 | "lint:js": "wp-scripts lint-js", 50 | "lint:js:fix": "npm run lint:js -- --fix", 51 | "lint:php": "composer run-script lint", 52 | "lint:php:fix": "composer run-script format", 53 | "lint:md-docs": "wp-scripts lint-md-docs", 54 | "lint:pkg-json": "wp-scripts lint-pkg-json", 55 | "test:php": "npm run test:php:setup && wp-env run tests-wordpress --env-cwd='wp-content/plugins/create-block-theme' composer run-script test", 56 | "test:php:watch": "wp-env run cli --env-cwd='wp-content/plugins/create-block-theme' composer run-script test:watch", 57 | "test:php:setup": "wp-env start", 58 | "packages-update": "wp-scripts packages-update", 59 | "start": "wp-scripts start src/admin-landing-page.js src/plugin-sidebar.js", 60 | "composer": "wp-env run cli --env-cwd=wp-content/plugins/create-block-theme composer", 61 | "update-version": "node update-version-and-changelog.js", 62 | "prepare": "husky install", 63 | "wp-env": "wp-env", 64 | "test:unit": "wp-scripts test-unit-js --config test/unit/jest.config.js" 65 | }, 66 | "lint-staged": { 67 | "*.{js,json,yml}": [ 68 | "wp-scripts format" 69 | ], 70 | "*.js": [ 71 | "npm run lint:js" 72 | ], 73 | "*.{css,scss}": [ 74 | "npm run lint:css" 75 | ], 76 | "*.php": [ 77 | "npm run lint:php" 78 | ], 79 | "*.md": [ 80 | "npm run lint:md-docs" 81 | ], 82 | "package.json": [ 83 | "npm run lint:pkg-json" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apply WordPress Coding Standards to all files 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | . 37 | 38 | 39 | 40 | 41 | 42 | 43 | warning 44 | 45 | 46 | warning 47 | 48 | 49 | warning 50 | 51 | 52 | warning 53 | 54 | 55 | warning 56 | 57 | 58 | 59 | /build/* 60 | /vendor/* 61 | /node_modules/* 62 | 63 | 64 | 65 | * 66 | 67 | 68 | 69 | 70 | * 71 | 72 | 73 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/ 14 | ./tests/test-sample.php 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/admin-landing-page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { createRoot } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import './admin-landing-page.scss'; 10 | import LandingPage from './landing-page/landing-page'; 11 | 12 | function App() { 13 | return ; 14 | } 15 | 16 | window.addEventListener( 17 | 'load', 18 | function () { 19 | const domNode = document.getElementById( 'create-block-theme-app' ); 20 | const root = createRoot( domNode ); 21 | root.render( ); 22 | }, 23 | false 24 | ); 25 | -------------------------------------------------------------------------------- /src/admin-landing-page.scss: -------------------------------------------------------------------------------- 1 | @import "../node_modules/@wordpress/base-styles/mixins"; 2 | @include wordpress-admin-schemes(); 3 | 4 | 5 | .create-block-theme { 6 | &__landing-page { 7 | background-color: #fff; 8 | margin-left: -20px; 9 | a, 10 | button { 11 | color: #3858e9; 12 | } 13 | &__header { 14 | width: 100%; 15 | background-color: #2d59f2; 16 | margin: 0; 17 | } 18 | &__body { 19 | padding: 40px 0; 20 | p { 21 | margin-top: 0; 22 | } 23 | h1, 24 | h2, 25 | h3, 26 | h4, 27 | h5, 28 | h6 { 29 | margin-top: 0.3em; 30 | margin-bottom: 0.3em; 31 | } 32 | h2 { 33 | font-size: 2em; 34 | } 35 | h3 { 36 | font-size: 1em; 37 | } 38 | @media screen and (max-width: 775px) { 39 | flex-direction: column; 40 | h2 { 41 | font-size: 1.5em; 42 | } 43 | } 44 | &__left-column { 45 | flex: 1; 46 | margin: 0 60px; 47 | button { 48 | font-size: 1.75em; 49 | @media screen and (max-width: 775px) { 50 | font-size: 1.25em; 51 | } 52 | } 53 | } 54 | &__right-column { 55 | max-width: 330px; 56 | margin: 0 60px; 57 | @media screen and (max-width: 775px) { 58 | max-width: 100%; 59 | } 60 | p { 61 | margin-bottom: 0; 62 | } 63 | } 64 | 65 | &__faq { 66 | img { 67 | max-width: 100%; 68 | } 69 | p { 70 | padding: 10px; 71 | font-style: italic; 72 | } 73 | details { 74 | padding-bottom: 20px; 75 | 76 | summary { 77 | cursor: pointer; 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/editor-sidebar/about.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { 6 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 7 | __experimentalVStack as VStack, 8 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 9 | __experimentalText as Text, 10 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 11 | __experimentalDivider as Divider, 12 | PanelBody, 13 | ExternalLink, 14 | } from '@wordpress/components'; 15 | 16 | /** 17 | * Internal dependencies 18 | */ 19 | import ScreenHeader from './screen-header'; 20 | 21 | function AboutPlugin() { 22 | return ( 23 | 24 | 27 | 28 | 29 | { __( 30 | 'Create Block Theme is a tool to help you make Block Themes using the WordPress Editor. It does this by adding tools to the Editor to help you create and manage your theme.', 31 | 'create-block-theme' 32 | ) } 33 | 34 | 35 | 36 | { __( 37 | "Themes created with Create Block Theme don't require Create Block Theme to be installed on the site where the theme is used.", 38 | 'create-block-theme' 39 | ) } 40 | 41 | 42 | 43 | 44 | 45 | { __( 'Help', 'create-block-theme' ) } 46 | 47 | 48 | 49 | <> 50 | { __( 'Have a question?', 'create-block-theme' ) } 51 |
52 | 53 | { __( 'Ask in the forums.', 'create-block-theme' ) } 54 | 55 | 56 |
57 | 58 | 59 | <> 60 | { __( 'Found a bug?', 'create-block-theme' ) } 61 |
62 | 63 | { __( 64 | 'Report it on GitHub.', 65 | 'create-block-theme' 66 | ) } 67 | 68 | 69 |
70 | 71 | 72 | <> 73 | { __( 'Want to contribute?', 'create-block-theme' ) } 74 |
75 | 76 | { __( 77 | 'Check out the project on GitHub.', 78 | 'create-block-theme' 79 | ) } 80 | 81 | 82 |
83 |
84 |
85 | ); 86 | } 87 | 88 | export default AboutPlugin; 89 | -------------------------------------------------------------------------------- /src/editor-sidebar/create-panel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { useState } from '@wordpress/element'; 6 | import { useDispatch } from '@wordpress/data'; 7 | import { store as noticesStore } from '@wordpress/notices'; 8 | import { 9 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 10 | __experimentalVStack as VStack, 11 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 12 | __experimentalSpacer as Spacer, 13 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 14 | __experimentalText as Text, 15 | PanelBody, 16 | Button, 17 | SelectControl, 18 | TextControl, 19 | TextareaControl, 20 | } from '@wordpress/components'; 21 | import { addCard, copy } from '@wordpress/icons'; 22 | 23 | /** 24 | * Internal dependencies 25 | */ 26 | import ScreenHeader from './screen-header'; 27 | import { 28 | createBlankTheme, 29 | createClonedTheme, 30 | createChildTheme, 31 | } from '../resolvers'; 32 | import { generateWpVersions } from '../utils/generate-versions'; 33 | 34 | const WP_MINIMUM_VERSIONS = generateWpVersions( WP_VERSION ); // eslint-disable-line no-undef 35 | 36 | export const CreateThemePanel = ( { createType } ) => { 37 | const { createErrorNotice } = useDispatch( noticesStore ); 38 | 39 | const [ theme, setTheme ] = useState( { 40 | name: '', 41 | description: '', 42 | uri: '', 43 | author: '', 44 | author_uri: '', 45 | tags_custom: '', 46 | requires_wp: '', 47 | } ); 48 | 49 | const cloneTheme = () => { 50 | if ( createType === 'createClone' ) { 51 | handleCloneClick(); 52 | } else if ( createType === 'createChild' ) { 53 | handleCreateChildClick(); 54 | } 55 | }; 56 | 57 | const handleCreateBlankClick = () => { 58 | createBlankTheme( theme ) 59 | .then( () => { 60 | // eslint-disable-next-line no-alert 61 | window.alert( 62 | __( 63 | 'Theme created successfully. The editor will now reload.', 64 | 'create-block-theme' 65 | ) 66 | ); 67 | window.location.reload(); 68 | } ) 69 | .catch( ( error ) => { 70 | const errorMessage = 71 | error.message || 72 | __( 73 | 'An error occurred while attempting to create the theme.', 74 | 'create-block-theme' 75 | ); 76 | createErrorNotice( errorMessage, { type: 'snackbar' } ); 77 | } ); 78 | }; 79 | 80 | const handleCloneClick = () => { 81 | createClonedTheme( theme ) 82 | .then( () => { 83 | // eslint-disable-next-line no-alert 84 | window.alert( 85 | __( 86 | 'Theme cloned successfully. The editor will now reload.', 87 | 'create-block-theme' 88 | ) 89 | ); 90 | window.location.reload(); 91 | } ) 92 | .catch( ( error ) => { 93 | const errorMessage = 94 | error.message || 95 | __( 96 | 'An error occurred while attempting to create the theme.', 97 | 'create-block-theme' 98 | ); 99 | createErrorNotice( errorMessage, { type: 'snackbar' } ); 100 | } ); 101 | }; 102 | 103 | const handleCreateChildClick = () => { 104 | createChildTheme( theme ) 105 | .then( () => { 106 | // eslint-disable-next-line no-alert 107 | window.alert( 108 | __( 109 | 'Child theme created successfully. The editor will now reload.', 110 | 'create-block-theme' 111 | ) 112 | ); 113 | window.location.reload(); 114 | } ) 115 | .catch( ( error ) => { 116 | const errorMessage = 117 | error.message || 118 | __( 119 | 'An error occurred while attempting to create the theme.', 120 | 'create-block-theme' 121 | ); 122 | createErrorNotice( errorMessage, { type: 'snackbar' } ); 123 | } ); 124 | }; 125 | 126 | return ( 127 | 128 | 131 | 132 | 138 | setTheme( { ...theme, name: value } ) 139 | } 140 | /> 141 |
142 | 143 | { __( 144 | 'Additional Theme MetaData', 145 | 'create-block-theme' 146 | ) } 147 | 148 | 149 | 150 | 158 | setTheme( { ...theme, description: value } ) 159 | } 160 | placeholder={ __( 161 | 'A short description of the theme', 162 | 'create-block-theme' 163 | ) } 164 | /> 165 | 171 | setTheme( { ...theme, uri: value } ) 172 | } 173 | placeholder={ __( 174 | 'https://github.com/wordpress/twentytwentythree/', 175 | 'create-block-theme' 176 | ) } 177 | /> 178 | 184 | setTheme( { ...theme, author: value } ) 185 | } 186 | placeholder={ __( 187 | 'the WordPress team', 188 | 'create-block-theme' 189 | ) } 190 | /> 191 | 197 | setTheme( { ...theme, author_uri: value } ) 198 | } 199 | placeholder={ __( 200 | 'https://wordpress.org/', 201 | 'create-block-theme' 202 | ) } 203 | /> 204 | ( { 214 | label: version, 215 | value: version, 216 | } ) 217 | ) } 218 | onChange={ ( value ) => { 219 | setTheme( { ...theme, requires_wp: value } ); 220 | } } 221 | /> 222 | 223 |
224 |
225 | { createType === 'createClone' && ( 226 | <> 227 | 234 | 235 | ) } 236 | { createType === 'createChild' && ( 237 | <> 238 | 245 | 246 | ) } 247 | { createType === 'createBlank' && ( 248 | <> 249 | 256 | 257 | 258 | { __( 259 | 'Create a blank theme with no styles or templates.', 260 | 'create-block-theme' 261 | ) } 262 | 263 | 264 | ) } 265 |
266 |
267 | ); 268 | }; 269 | -------------------------------------------------------------------------------- /src/editor-sidebar/create-variation-panel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { useState } from '@wordpress/element'; 6 | import { useDispatch, useSelect } from '@wordpress/data'; 7 | import { store as noticesStore } from '@wordpress/notices'; 8 | import { 9 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 10 | __experimentalVStack as VStack, 11 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 12 | __experimentalText as Text, 13 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 14 | __experimentalSpacer as Spacer, 15 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 16 | __experimentalView as View, 17 | PanelBody, 18 | Button, 19 | TextControl, 20 | CheckboxControl, 21 | } from '@wordpress/components'; 22 | import { copy } from '@wordpress/icons'; 23 | import { store as preferencesStore } from '@wordpress/preferences'; 24 | 25 | /** 26 | * Internal dependencies 27 | */ 28 | import { postCreateThemeVariation } from '../resolvers'; 29 | import ScreenHeader from './screen-header'; 30 | 31 | const PREFERENCE_SCOPE = 'create-block-theme'; 32 | const PREFERENCE_KEY = 'create-variation'; 33 | 34 | export const CreateVariationPanel = () => { 35 | const { createErrorNotice } = useDispatch( noticesStore ); 36 | 37 | const [ theme, setTheme ] = useState( { 38 | name: '', 39 | } ); 40 | 41 | const preference = useSelect( ( select ) => { 42 | const _preference = select( preferencesStore ).get( 43 | PREFERENCE_SCOPE, 44 | PREFERENCE_KEY 45 | ); 46 | return { 47 | saveFonts: _preference?.saveFonts ?? true, 48 | }; 49 | }, [] ); 50 | 51 | const handleTogglePreference = ( key ) => { 52 | setPreference( PREFERENCE_SCOPE, PREFERENCE_KEY, { 53 | ...preference, 54 | [ key ]: ! preference[ key ], 55 | } ); 56 | }; 57 | 58 | const { set: setPreference } = useDispatch( preferencesStore ); 59 | 60 | const handleCreateVariationClick = () => { 61 | const variationPreferences = { 62 | name: theme.name, 63 | ...preference, 64 | }; 65 | 66 | postCreateThemeVariation( variationPreferences ) 67 | .then( () => { 68 | // eslint-disable-next-line no-alert 69 | window.alert( 70 | __( 71 | 'Theme variation created successfully. The editor will now reload.', 72 | 'create-block-theme' 73 | ) 74 | ); 75 | window.location.reload(); 76 | } ) 77 | .catch( ( error ) => { 78 | const errorMessage = 79 | error.message || 80 | __( 81 | 'An error occurred while attempting to create the theme variation.', 82 | 'create-block-theme' 83 | ); 84 | createErrorNotice( errorMessage, { type: 'snackbar' } ); 85 | } ); 86 | }; 87 | 88 | return ( 89 | 90 | 93 | 94 | 95 | 96 | { __( 97 | 'Save the Global Styles changes as a theme variation.', 98 | 'create-block-theme' 99 | ) } 100 | 101 | 102 | 103 | 104 | 105 | 114 | setTheme( { ...theme, name: value } ) 115 | } 116 | /> 117 | 118 | 130 | handleTogglePreference( 'saveFonts' ) 131 | } 132 | /> 133 | 134 | 144 | 145 | 146 | 147 | 148 | 149 | ); 150 | }; 151 | -------------------------------------------------------------------------------- /src/editor-sidebar/global-styles-json-editor-modal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import CodeMirror from '@uiw/react-codemirror'; 5 | import { json } from '@codemirror/lang-json'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import { __, sprintf } from '@wordpress/i18n'; 11 | import { Modal } from '@wordpress/components'; 12 | import { useSelect } from '@wordpress/data'; 13 | import { store as coreStore } from '@wordpress/core-data'; 14 | 15 | const GlobalStylesJsonEditorModal = ( { onRequestClose } ) => { 16 | const themeName = useSelect( ( select ) => 17 | select( 'core' ).getCurrentTheme() 18 | )?.name?.raw; 19 | 20 | const { record: globalStylesRecord } = useSelect( ( select ) => { 21 | const { 22 | __experimentalGetCurrentGlobalStylesId, 23 | getEditedEntityRecord, 24 | } = select( coreStore ); 25 | const globalStylesId = __experimentalGetCurrentGlobalStylesId(); 26 | const record = getEditedEntityRecord( 27 | 'root', 28 | 'globalStyles', 29 | globalStylesId 30 | ); 31 | return { 32 | record, 33 | }; 34 | } ); 35 | 36 | const globalStyles = { 37 | ...( globalStylesRecord?.styles && { 38 | styles: globalStylesRecord.styles, 39 | } ), 40 | ...( globalStylesRecord?.settings && { 41 | settings: globalStylesRecord.settings, 42 | } ), 43 | }; 44 | 45 | const globalStylesAsString = globalStyles 46 | ? JSON.stringify( globalStyles, null, 4 ) 47 | : ''; 48 | 49 | const handleSave = () => {}; 50 | 51 | return ( 52 | 62 | 68 | 69 | ); 70 | }; 71 | 72 | export default GlobalStylesJsonEditorModal; 73 | -------------------------------------------------------------------------------- /src/editor-sidebar/json-editor-modal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import CodeMirror from '@uiw/react-codemirror'; 5 | import { json } from '@codemirror/lang-json'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import { __, sprintf } from '@wordpress/i18n'; 11 | import { useState, useEffect } from '@wordpress/element'; 12 | import { Modal } from '@wordpress/components'; 13 | import { useSelect } from '@wordpress/data'; 14 | 15 | /** 16 | * Internal dependencies 17 | */ 18 | import { fetchThemeJson } from '../resolvers'; 19 | 20 | const ThemeJsonEditorModal = ( { onRequestClose } ) => { 21 | const [ themeData, setThemeData ] = useState( '' ); 22 | const themeName = useSelect( ( select ) => 23 | select( 'core' ).getCurrentTheme() 24 | )?.name?.raw; 25 | const fetchThemeData = async () => { 26 | setThemeData( await fetchThemeJson() ); 27 | }; 28 | const handleSave = () => {}; 29 | 30 | useEffect( () => { 31 | fetchThemeData(); 32 | } ); 33 | 34 | return ( 35 | 45 | 51 | 52 | ); 53 | }; 54 | 55 | export default ThemeJsonEditorModal; 56 | -------------------------------------------------------------------------------- /src/editor-sidebar/reset-theme.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { useDispatch, useSelect } from '@wordpress/data'; 6 | import { store as noticesStore } from '@wordpress/notices'; 7 | import { 8 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 9 | __experimentalConfirmDialog as ConfirmDialog, 10 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 11 | __experimentalVStack as VStack, 12 | PanelBody, 13 | Button, 14 | CheckboxControl, 15 | } from '@wordpress/components'; 16 | import { trash } from '@wordpress/icons'; 17 | import { useState } from '@wordpress/element'; 18 | import { store as preferencesStore } from '@wordpress/preferences'; 19 | 20 | /** 21 | * Internal dependencies 22 | */ 23 | import ScreenHeader from './screen-header'; 24 | import { resetTheme } from '../resolvers'; 25 | 26 | const PREFERENCE_SCOPE = 'create-block-theme'; 27 | const PREFERENCE_KEY = 'reset-theme'; 28 | 29 | function ResetTheme() { 30 | const preferences = useSelect( ( select ) => { 31 | const _preference = select( preferencesStore ).get( 32 | PREFERENCE_SCOPE, 33 | PREFERENCE_KEY 34 | ); 35 | return { 36 | resetStyles: _preference?.resetStyles ?? true, 37 | resetTemplates: _preference?.resetTemplates ?? true, 38 | resetTemplateParts: _preference?.resetTemplateParts ?? true, 39 | }; 40 | }, [] ); 41 | 42 | const { set: setPreferences } = useDispatch( preferencesStore ); 43 | const { createErrorNotice } = useDispatch( noticesStore ); 44 | const [ isConfirmDialogOpen, setIsConfirmDialogOpen ] = useState( false ); 45 | 46 | const handleTogglePreference = ( key ) => { 47 | setPreferences( PREFERENCE_SCOPE, PREFERENCE_KEY, { 48 | ...preferences, 49 | [ key ]: ! preferences[ key ], 50 | } ); 51 | }; 52 | 53 | const toggleConfirmDialog = () => { 54 | setIsConfirmDialogOpen( ! isConfirmDialogOpen ); 55 | }; 56 | 57 | const handleResetTheme = async () => { 58 | try { 59 | await resetTheme( preferences ); 60 | toggleConfirmDialog(); 61 | // eslint-disable-next-line no-alert 62 | window.alert( 63 | __( 64 | 'Theme reset successfully. The editor will now reload.', 65 | 'create-block-theme' 66 | ) 67 | ); 68 | window.location.reload(); 69 | } catch ( error ) { 70 | createErrorNotice( 71 | __( 72 | 'An error occurred while resetting the theme.', 73 | 'create-block-theme' 74 | ) 75 | ); 76 | } 77 | }; 78 | 79 | return ( 80 | <> 81 | 88 | { __( 89 | 'Are you sure you want to reset the theme? This action cannot be undone.', 90 | 'create-block-theme' 91 | ) } 92 | 93 | 94 | 97 | 98 | 110 | handleTogglePreference( 'resetStyles' ) 111 | } 112 | /> 113 | 114 | 126 | handleTogglePreference( 'resetTemplates' ) 127 | } 128 | /> 129 | 130 | 142 | handleTogglePreference( 'resetTemplateParts' ) 143 | } 144 | /> 145 | 146 | 250 | 251 | 252 | ); 253 | }; 254 | -------------------------------------------------------------------------------- /src/editor-sidebar/screen-header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { 5 | Navigator, 6 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 7 | __experimentalHStack as HStack, 8 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 9 | __experimentalSpacer as Spacer, 10 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 11 | __experimentalHeading as Heading, 12 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 13 | __experimentalNavigatorToParentButton as NavigatorToParentButton, 14 | } from '@wordpress/components'; 15 | import { isRTL, __ } from '@wordpress/i18n'; 16 | import { chevronRight, chevronLeft } from '@wordpress/icons'; 17 | 18 | const ScreenHeader = ( { title, onBack } ) => { 19 | // TODO: Remove the fallback component when the minimum supported WordPress 20 | // version was increased to 6.7. 21 | const BackButton = Navigator?.BackButton || NavigatorToParentButton; 22 | return ( 23 | 24 | 25 | 32 | 33 | 39 | { title } 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default ScreenHeader; 48 | -------------------------------------------------------------------------------- /src/landing-page/create-modal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { useState } from '@wordpress/element'; 6 | import { 7 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 8 | __experimentalHStack as HStack, 9 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 10 | __experimentalVStack as VStack, 11 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 12 | __experimentalText as Text, 13 | Modal, 14 | Button, 15 | TextControl, 16 | TextareaControl, 17 | } from '@wordpress/components'; 18 | 19 | /** 20 | * Internal dependencies 21 | */ 22 | import { 23 | createBlankTheme, 24 | createClonedTheme, 25 | createChildTheme, 26 | } from '../resolvers'; 27 | 28 | export const CreateThemeModal = ( { onRequestClose, creationType } ) => { 29 | const [ errorMessage, setErrorMessage ] = useState( null ); 30 | 31 | const [ theme, setTheme ] = useState( { 32 | name: '', 33 | description: '', 34 | author: '', 35 | } ); 36 | 37 | const renderCreateButtonText = ( type ) => { 38 | switch ( type ) { 39 | case 'blank': 40 | return __( 41 | 'Create and Activate Blank Theme', 42 | 'create-block-theme' 43 | ); 44 | case 'clone': 45 | return __( 'Clone Block Theme', 'create-block-theme' ); 46 | case 'child': 47 | return __( 'Create Child Theme', 'create-block-theme' ); 48 | } 49 | }; 50 | 51 | const createBlockTheme = async () => { 52 | let constructionFunction = null; 53 | switch ( creationType ) { 54 | case 'blank': 55 | constructionFunction = createBlankTheme; 56 | break; 57 | case 'clone': 58 | constructionFunction = createClonedTheme; 59 | break; 60 | case 'child': 61 | constructionFunction = createChildTheme; 62 | break; 63 | } 64 | 65 | if ( ! constructionFunction ) { 66 | return; 67 | } 68 | constructionFunction( theme ) 69 | .then( () => { 70 | // eslint-disable-next-line no-alert 71 | window.alert( 72 | __( 73 | 'Theme created successfully. The editor will now load.', 74 | 'create-block-theme' 75 | ) 76 | ); 77 | window.location = window.cbt_landingpage_variables.editor_url; 78 | } ) 79 | .catch( ( error ) => { 80 | setErrorMessage( 81 | error.message || 82 | __( 83 | 'An error occurred while attempting to create the theme.', 84 | 'create-block-theme' 85 | ) 86 | ); 87 | } ); 88 | }; 89 | 90 | if ( errorMessage ) { 91 | return ( 92 | 96 |

{ errorMessage }

97 |
98 | ); 99 | } 100 | 101 | return ( 102 | 106 | 107 | 108 | { __( 109 | "Let's get started creating a new Block Theme.", 110 | 'create-block-theme' 111 | ) } 112 | 113 | 123 | setTheme( { ...theme, name: value } ) 124 | } 125 | help={ __( 126 | '(Tip: You can edit all of this and more in the Editor later.)', 127 | 'create-block-theme' 128 | ) } 129 | /> 130 | 135 | setTheme( { ...theme, description: value } ) 136 | } 137 | placeholder={ __( 138 | 'A short description of the theme', 139 | 'create-block-theme' 140 | ) } 141 | /> 142 | 148 | setTheme( { ...theme, author: value } ) 149 | } 150 | placeholder={ __( 151 | 'the WordPress team', 152 | 'create-block-theme' 153 | ) } 154 | /> 155 | 156 | 163 | 164 | 165 | 166 | ); 167 | }; 168 | -------------------------------------------------------------------------------- /src/landing-page/landing-page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { sprintf, __ } from '@wordpress/i18n'; 5 | import { useState, createInterpolateElement } from '@wordpress/element'; 6 | import { store as coreStore } from '@wordpress/core-data'; 7 | import { useSelect } from '@wordpress/data'; 8 | import { 9 | Button, 10 | ExternalLink, 11 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 12 | __experimentalVStack as VStack, 13 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 14 | __experimentalHStack as HStack, 15 | } from '@wordpress/components'; 16 | 17 | /** 18 | * Internal dependencies 19 | */ 20 | import { downloadExportedTheme } from '../resolvers'; 21 | import downloadFile from '../utils/download-file'; 22 | import { CreateThemeModal } from './create-modal'; 23 | 24 | export default function LandingPage() { 25 | const [ createModalType, setCreateModalType ] = useState( false ); 26 | 27 | const themeName = useSelect( ( select ) => 28 | select( coreStore ).getCurrentTheme() 29 | )?.name?.raw; 30 | 31 | const handleExportClick = async () => { 32 | const response = await downloadExportedTheme(); 33 | downloadFile( response ); 34 | }; 35 | 36 | return ( 37 |
38 | { createModalType && ( 39 | setCreateModalType( false ) } 42 | /> 43 | ) } 44 | 45 |

46 | { 53 |

54 | 55 | 59 | 63 |

64 | { __( 65 | 'What would you like to do?', 66 | 'create-block-theme' 67 | ) } 68 |

69 |

70 | { createInterpolateElement( 71 | __( 72 | 'You can do everything from within the Editor but here are a few things you can do to get started.', 73 | 'create-block-theme' 74 | ), 75 | { 76 | a: ( 77 | // eslint-disable-next-line jsx-a11y/anchor-has-content 78 | 84 | ), 85 | } 86 | ) } 87 |

88 | 101 |

102 | { __( 103 | 'Export a zip file ready to be imported into another WordPress environment.', 104 | 'create-block-theme' 105 | ) } 106 |

107 | 116 |

117 | { __( 118 | 'Start from scratch! Create a blank theme to get started with your own design ideas.', 119 | 'create-block-theme' 120 | ) } 121 |

122 | 135 |

136 | { __( 137 | 'Use the currently activated theme as a starting point.', 138 | 'create-block-theme' 139 | ) } 140 |

141 | 154 |

155 | { __( 156 | 'Make a theme that uses the currently activated theme as a parent.', 157 | 'create-block-theme' 158 | ) } 159 |

160 |
161 | 162 |

{ __( 'About the Plugin', 'create-block-theme' ) }

163 |

164 | { __( 165 | "Create Block Theme is a tool to help you make Block Themes using the WordPress Editor. It does this by adding tools to the Editor to help you create and manage your theme. Themes created with Create Block Theme don't require Create Block Theme to be installed on the site where the theme is used.", 166 | 'create-block-theme' 167 | ) } 168 |

169 |

170 | { __( 'Do you need some help?', 'create-block-theme' ) } 171 |

172 |

173 | { createInterpolateElement( 174 | __( 175 | 'Have a question? Ask for some help in the forums.', 176 | 'create-block-theme' 177 | ), 178 | { 179 | ExternalLink: ( 180 | 186 | ), 187 | } 188 | ) } 189 |

190 |

191 | { createInterpolateElement( 192 | __( 193 | 'Found a bug? Report it on GitHub.', 194 | 'create-block-theme' 195 | ), 196 | { 197 | ExternalLink: ( 198 | 199 | ), 200 | } 201 | ) } 202 |

203 |

204 | { createInterpolateElement( 205 | __( 206 | 'Want to contribute? Check out the project on GitHub.', 207 | 'create-block-theme' 208 | ), 209 | { 210 | ExternalLink: ( 211 | 212 | ), 213 | } 214 | ) } 215 |

216 |
217 |

{ __( 'FAQ', 'create-block-theme' ) }

218 |
219 | 220 | { __( 221 | 'How do I access the features of Create Block Theme from within the editor?', 222 | 'create-block-theme' 223 | ) } 224 | 225 |

226 | { __( 227 | 'There is a new panel accessible from the WordPress Editor which you can open by clicking on a new icon to the right of the “Save” button, at the top of the Editor.', 228 | 'create-block-theme' 229 | ) } 230 |

231 | { 241 |
242 |
243 | 244 | { __( 245 | 'How do I save the customizations I made with the Editor to the Theme?', 246 | 'create-block-theme' 247 | ) } 248 | 249 |

250 | { __( 251 | 'In the Create Block Theme Panel click "Save Changes to Theme". You will be presented with a number of options of which things you want to be saved to your theme. Make your choices and then click "Save Changes".', 252 | 'create-block-theme' 253 | ) } 254 |

255 | { 265 |
266 |
267 | 268 | { __( 269 | 'How do I install and remove fonts?', 270 | 'create-block-theme' 271 | ) } 272 | 273 |

274 | { __( 275 | 'First Install and activate a font from any source using the WordPress Font Library. Then, using the Create Block Theme Panel select “Save Changes To Theme” and select “Save Fonts” before saving the theme. All of the active fonts will be activated in the theme and deactivated in the system (and may be safely deleted from the system). Any fonts that are installed in the theme that have been deactivated with the WordPress Font Library will be removed from the theme.', 276 | 'create-block-theme' 277 | ) } 278 |

279 | { 289 |
290 |
291 |
292 |
293 |
294 | ); 295 | } 296 | -------------------------------------------------------------------------------- /src/plugin-styles.scss: -------------------------------------------------------------------------------- 1 | @import "~@wordpress/base-styles/colors"; 2 | 3 | $plugin-prefix: "create-block-theme"; 4 | $modal-footer-height: 70px; 5 | 6 | .#{$plugin-prefix} { 7 | &__metadata-editor-modal { 8 | padding-bottom: $modal-footer-height; 9 | 10 | &__footer { 11 | border-top: 1px solid #ddd; 12 | background-color: #fff; 13 | position: absolute; 14 | bottom: 0; 15 | margin: 0 -32px; 16 | padding: 16px 32px; 17 | height: $modal-footer-height; 18 | } 19 | 20 | &__screenshot { 21 | max-width: 200px; 22 | height: auto; 23 | aspect-ratio: 4 / 3; 24 | object-fit: cover; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/resolvers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import apiFetch from '@wordpress/api-fetch'; 5 | 6 | export async function fetchThemeJson() { 7 | return apiFetch( { 8 | path: '/create-block-theme/v1/get-theme-data', 9 | method: 'GET', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | } ).then( ( response ) => { 14 | if ( ! response?.data || 'SUCCESS' !== response?.status ) { 15 | throw new Error( 16 | `Failed to fetch theme data: ${ 17 | response?.message || response?.status 18 | }` 19 | ); 20 | } 21 | return JSON.stringify( response?.data, null, 2 ); 22 | } ); 23 | } 24 | 25 | export async function createBlankTheme( theme ) { 26 | return apiFetch( { 27 | path: '/create-block-theme/v1/create-blank', 28 | method: 'POST', 29 | data: theme, 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | }, 33 | } ).then( ( response ) => { 34 | if ( 'SUCCESS' !== response?.status ) { 35 | throw new Error( 36 | `Failed to create blank theme: ${ 37 | response?.message || response?.status 38 | }` 39 | ); 40 | } 41 | return response; 42 | } ); 43 | } 44 | 45 | export async function createClonedTheme( theme ) { 46 | return apiFetch( { 47 | path: '/create-block-theme/v1/clone', 48 | method: 'POST', 49 | data: theme, 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | }, 53 | } ).then( ( response ) => { 54 | if ( 'SUCCESS' !== response?.status ) { 55 | throw new Error( 56 | `Failed to clone theme: ${ 57 | response?.message || response?.status 58 | }` 59 | ); 60 | } 61 | return response; 62 | } ); 63 | } 64 | 65 | export async function createChildTheme( theme ) { 66 | return apiFetch( { 67 | path: '/create-block-theme/v1/create-child', 68 | method: 'POST', 69 | data: theme, 70 | headers: { 71 | 'Content-Type': 'application/json', 72 | }, 73 | } ).then( ( response ) => { 74 | if ( 'SUCCESS' !== response?.status ) { 75 | throw new Error( 76 | `Failed to create child theme: ${ 77 | response?.message || response?.status 78 | }` 79 | ); 80 | } 81 | return response; 82 | } ); 83 | } 84 | 85 | export async function fetchReadmeData() { 86 | return apiFetch( { 87 | path: '/create-block-theme/v1/get-readme-data', 88 | method: 'GET', 89 | headers: { 90 | 'Content-Type': 'application/json', 91 | }, 92 | } ).then( ( response ) => { 93 | if ( ! response?.data || 'SUCCESS' !== response?.status ) { 94 | throw new Error( 95 | `Failed to fetch readme data: ${ 96 | response?.message || response?.status 97 | }` 98 | ); 99 | } 100 | return response?.data; 101 | } ); 102 | } 103 | 104 | export async function postCreateThemeVariation( preferences ) { 105 | return apiFetch( { 106 | path: '/create-block-theme/v1/create-variation', 107 | method: 'POST', 108 | data: preferences, 109 | headers: { 110 | 'Content-Type': 'application/json', 111 | }, 112 | } ); 113 | } 114 | 115 | export async function postUpdateThemeMetadata( theme ) { 116 | return apiFetch( { 117 | path: '/create-block-theme/v1/update', 118 | method: 'POST', 119 | data: theme, 120 | headers: { 121 | 'Content-Type': 'application/json', 122 | }, 123 | } ); 124 | } 125 | 126 | export async function downloadExportedTheme() { 127 | return apiFetch( { 128 | path: '/create-block-theme/v1/export', 129 | method: 'POST', 130 | headers: { 131 | 'Content-Type': 'application/json', 132 | }, 133 | parse: false, 134 | } ); 135 | } 136 | 137 | export async function getFontFamilies() { 138 | const response = await apiFetch( { 139 | path: '/create-block-theme/v1/font-families', 140 | method: 'GET', 141 | headers: { 142 | 'Content-Type': 'application/json', 143 | }, 144 | } ); 145 | return response.data; 146 | } 147 | 148 | export async function resetTheme( preferences ) { 149 | return apiFetch( { 150 | path: '/create-block-theme/v1/reset-theme', 151 | method: 'PATCH', 152 | data: preferences, 153 | headers: { 154 | 'Content-Type': 'application/json', 155 | }, 156 | } ).then( ( response ) => { 157 | if ( 'SUCCESS' !== response?.status ) { 158 | throw new Error( 159 | `Failed to reset theme: ${ 160 | response?.message || response?.status 161 | }` 162 | ); 163 | } 164 | return response; 165 | } ); 166 | } 167 | -------------------------------------------------------------------------------- /src/test/unit.js: -------------------------------------------------------------------------------- 1 | // TODO: Add unit tests as needed 2 | 3 | describe( 'Sample Unit Test', function () { 4 | it( 'should pass', function () { 5 | expect( true ).toBe( true ); 6 | } ); 7 | } ); 8 | -------------------------------------------------------------------------------- /src/utils/download-file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { downloadBlob } from '@wordpress/blob'; 5 | 6 | /* 7 | * Download a file from in a browser. 8 | * 9 | * @param {Response} response The response object from a fetch request. 10 | * @return {void} 11 | */ 12 | export default async function downloadFile( response ) { 13 | const blob = await response.blob(); 14 | const filename = response.headers 15 | .get( 'Content-Disposition' ) 16 | .split( 'filename=' )[ 1 ]; 17 | downloadBlob( filename, blob, 'application/zip' ); 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/fonts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { getFontFamilies } from '../resolvers'; 5 | import { Font } from '../lib/lib-font/lib-font.browser'; 6 | 7 | /** 8 | * Fetch a file from a URL and return it as an ArrayBuffer. 9 | * 10 | * @param {string} url The URL of the file to fetch. 11 | * @return {Promise} The file as an ArrayBuffer. 12 | */ 13 | async function fetchFileAsArrayBuffer( url ) { 14 | const response = await fetch( url ); 15 | if ( ! response.ok ) { 16 | throw new Error( 'Network response was not ok.' ); 17 | } 18 | const arrayBuffer = await response.arrayBuffer(); 19 | return arrayBuffer; 20 | } 21 | 22 | /** 23 | * Retrieves the licensing information of a font file given its URL. 24 | * 25 | * This function fetches the file as an ArrayBuffer, initializes a font object, and extracts licensing details from the font's OpenType tables. 26 | * 27 | * @param {string} url - The URL pointing directly to the font file. The URL should be a direct link to the file and publicly accessible. 28 | * @return {Promise} A promise that resolves to an object containing the font's licensing details. 29 | * 30 | * The returned object includes the following properties (if available in the font's OpenType tables): 31 | * - fontName: The full font name. 32 | * - copyright: Copyright notice. 33 | * - source: Unique identifier for the font's source. 34 | * - license: License description. 35 | * - licenseURL: URL to the full license text. 36 | */ 37 | async function getFontFileLicenseFromUrl( url ) { 38 | const buffer = await fetchFileAsArrayBuffer( url ); 39 | const fontObj = new Font( 'Uploaded Font' ); 40 | fontObj.fromDataBuffer( buffer, url ); 41 | // Assuming that fromDataBuffer triggers onload event and returning a Promise 42 | const onloadEvent = await new Promise( 43 | ( resolve ) => ( fontObj.onload = resolve ) 44 | ); 45 | const font = onloadEvent.detail.font; 46 | const { name: nameTable } = font.opentype.tables; 47 | return { 48 | fontName: nameTable.get( 16 ) || nameTable.get( 1 ), 49 | copyright: nameTable.get( 0 ), 50 | source: nameTable.get( 11 ), 51 | license: nameTable.get( 13 ), 52 | licenseURL: nameTable.get( 14 ), 53 | }; 54 | } 55 | 56 | /** 57 | * Get the license for a font family. 58 | * 59 | * @param {Object} fontFamily The font family in theme.json format. 60 | * @return {Promise} A promise that resolved to the font license object if sucessful or null if the font family does not have a fontFace property. 61 | */ 62 | async function getFamilyLicense( fontFamily ) { 63 | // If the font family does not have a fontFace property, return an empty string. 64 | if ( ! fontFamily.fontFace?.length ) { 65 | return null; 66 | } 67 | 68 | // Load the fontFace from the first fontFace object in the font family. 69 | const fontFace = fontFamily.fontFace[ 0 ]; 70 | const faceUrl = Array.isArray( fontFace.src ) 71 | ? fontFace.src[ 0 ] 72 | : fontFace.src; 73 | 74 | // Get the license from the font face url. 75 | return await getFontFileLicenseFromUrl( faceUrl ); 76 | } 77 | 78 | /** 79 | * Get the text for the font licenses of all the fonts defined in the theme. 80 | * 81 | * @return {Promise} A promise that resolves to an array containing font credits objects. 82 | */ 83 | async function getFontsCreditsArray() { 84 | const fontFamilies = await getFontFamilies(); 85 | 86 | //Remove duplicates. Removes the font families that have the same fontFamily property. 87 | const uniqueFontFamilies = fontFamilies.filter( 88 | ( fontFamily, index, self ) => 89 | index === 90 | self.findIndex( ( t ) => t.fontFamily === fontFamily.fontFamily ) 91 | ); 92 | 93 | const credits = []; 94 | 95 | // Iterate over fontFamilies and get the license for each family 96 | for ( const fontFamily of uniqueFontFamilies ) { 97 | const fontCredits = await getFamilyLicense( fontFamily ); 98 | if ( fontCredits ) { 99 | credits.push( fontCredits ); 100 | } 101 | } 102 | 103 | return credits; 104 | } 105 | 106 | /** 107 | * Get the text for the font licenses of all the fonts defined in the theme. 108 | * 109 | * @return {Promise} A promise that resolves to an string containing the formatted font licenses. 110 | */ 111 | export async function getFontsCreditsText() { 112 | const creditsArray = await getFontsCreditsArray(); 113 | const credits = creditsArray 114 | .reduce( ( acc, credit ) => { 115 | // skip if fontName is not available 116 | if ( ! credit.fontName ) { 117 | // continue 118 | return acc; 119 | } 120 | 121 | acc.push( credit.fontName ); 122 | 123 | if ( credit.copyright ) { 124 | acc.push( credit.copyright ); 125 | } 126 | 127 | if ( credit.source ) { 128 | acc.push( `Source: ${ credit.source }` ); 129 | } 130 | 131 | if ( credit.license ) { 132 | acc.push( `License: ${ credit.license }` ); 133 | } 134 | 135 | acc.push( '' ); 136 | 137 | return acc; 138 | }, [] ) 139 | .join( '\n' ); 140 | return credits; 141 | } 142 | -------------------------------------------------------------------------------- /src/utils/generate-versions.js: -------------------------------------------------------------------------------- 1 | export function generateWpVersions( versionString ) { 2 | const version = versionString.split( '-' )[ 0 ]; 3 | let [ major, minor ] = version.split( '.' ).slice( 0, 2 ).map( Number ); 4 | 5 | const versions = []; 6 | 7 | // Iterate through the versions from current to 5.9 8 | while ( major > 5 || ( major === 5 && minor >= 9 ) ) { 9 | versions.push( `${ major }.${ minor }` ); 10 | 11 | // Decrement minor version 12 | if ( minor === 0 ) { 13 | minor = 9; // Wrap around if minor is 0, decrement the major version 14 | major--; 15 | } else { 16 | minor--; 17 | } 18 | } 19 | 20 | return versions; 21 | } 22 | -------------------------------------------------------------------------------- /test/unit/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | rootDir: '../../', 4 | testMatch: [ '/src/test/**/*.js' ], 5 | moduleFileExtensions: [ 'js' ], 6 | moduleNameMapper: { 7 | '^@/(.*)$': '/src/$1', 8 | }, 9 | setupFiles: [ '/test/unit/setup.js' ], 10 | }; 11 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | // Stub out the FontFace class for tests. 2 | global.FontFace = class { 3 | constructor( family, source ) { 4 | this.family = family; 5 | this.source = source; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /tests/CbtThemeLocale/base.php: -------------------------------------------------------------------------------- 1 | orig_active_theme_slug = get_option( 'stylesheet' ); 31 | 32 | // Create a test theme directory. 33 | $this->test_theme_dir = DIR_TESTDATA . '/themes/'; 34 | 35 | // Register test theme directory. 36 | register_theme_directory( $this->test_theme_dir ); 37 | 38 | // Switch to the test theme. 39 | switch_theme( 'test-theme-locale' ); 40 | } 41 | 42 | /** 43 | * Tears down tests. 44 | */ 45 | public function tear_down() { 46 | parent::tear_down(); 47 | 48 | // Restore the original active theme. 49 | switch_theme( $this->orig_active_theme_slug ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/CbtThemeLocale/escapeAttribute.php: -------------------------------------------------------------------------------- 1 | getMethod( $method_name ); 16 | $method->setAccessible( true ); 17 | return $method->invokeArgs( null, $args ); 18 | } 19 | 20 | public function test_escape_attribute() { 21 | $string = 'This is a test attribute.'; 22 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) ); 23 | $expected_string = "get( 'TextDomain' ) . "');?>"; 24 | $this->assertEquals( $expected_string, $escaped_string ); 25 | } 26 | 27 | public function test_escape_attribute_with_single_quote() { 28 | $string = "This is a test attribute with a single quote '"; 29 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) ); 30 | $expected_string = "get( 'TextDomain' ) . "');?>"; 31 | $this->assertEquals( $expected_string, $escaped_string ); 32 | } 33 | 34 | public function test_escape_attribute_with_double_quote() { 35 | $string = 'This is a test attribute with a double quote "'; 36 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) ); 37 | $expected_string = "get( 'TextDomain' ) . "');?>"; 38 | $this->assertEquals( $expected_string, $escaped_string ); 39 | } 40 | 41 | public function test_escape_attribute_with_empty_string() { 42 | $string = ''; 43 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) ); 44 | $this->assertEquals( $string, $escaped_string ); 45 | } 46 | 47 | public function test_escape_attribute_with_already_escaped_string() { 48 | $string = "get( 'TextDomain' ) . "');?>"; 49 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) ); 50 | $this->assertEquals( $string, $escaped_string ); 51 | } 52 | 53 | public function test_escape_attribute_with_non_string() { 54 | $string = null; 55 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) ); 56 | $this->assertEquals( $string, $escaped_string ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/CbtThemeLocale/escapeTextContent.php: -------------------------------------------------------------------------------- 1 | getMethod( $method_name ); 17 | $method->setAccessible( true ); 18 | return $method->invokeArgs( null, $args ); 19 | } 20 | 21 | public function test_escape_text_content() { 22 | $string = 'This is a test text.'; 23 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) ); 24 | $this->assertEquals( "", $escaped_string ); 25 | } 26 | 27 | public function test_escape_text_content_with_single_quote() { 28 | $string = "This is a test text with a single quote '"; 29 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) ); 30 | $this->assertEquals( "", $escaped_string ); 31 | } 32 | 33 | public function test_escape_text_content_with_double_quote() { 34 | $string = 'This is a test text with a double quote "'; 35 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) ); 36 | $this->assertEquals( "", $escaped_string ); 37 | } 38 | 39 | public function test_escape_text_content_with_html() { 40 | $string = '

This is a test text with HTML.

'; 41 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) ); 42 | $expected_output = '\', \'

\' ); ?>'; 43 | $this->assertEquals( $expected_output, $escaped_string ); 44 | } 45 | 46 | public function test_escape_text_content_with_already_escaped_string() { 47 | $string = ""; 48 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) ); 49 | $this->assertEquals( $string, $escaped_string ); 50 | } 51 | 52 | public function test_escape_text_content_with_non_string() { 53 | $string = null; 54 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) ); 55 | $this->assertEquals( $string, $escaped_string ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/CbtThemeReadme/addOrUpdateSection.php: -------------------------------------------------------------------------------- 1 | assertStringContainsString( $section_title, $readme, 'The section title is missing.' ); 23 | $this->assertStringContainsString( $section_content, $readme, 'The section content is missing' ); 24 | 25 | // Update the section. 26 | $section_content_updated = 'Updated content xyz890'; 27 | 28 | $readme = CBT_Theme_Readme::add_or_update_section( $section_title, $section_content_updated ); 29 | 30 | // Check if the old content was updated. 31 | $this->assertStringNotContainsString( $section_content, $readme, 'The old content is still present.' ); 32 | 33 | // Check if the new content was added. 34 | $this->assertStringContainsString( $section_title, $readme, 'The section title is missing.' ); 35 | $this->assertStringContainsString( $section_content_updated, $readme, 'The updated content is missing.' ); 36 | 37 | // Check if that the section title was added only once. 38 | $section_count = substr_count( $readme, $section_title ); 39 | $this->assertEquals( 1, $section_count, 'The section title was added more than once.' ); 40 | } 41 | 42 | public function test_add_or_update_section_with_no_content() { 43 | $section_title = 'Test Section'; 44 | $section_content = ''; 45 | 46 | // Empty section should not be added. 47 | $readme = CBT_Theme_Readme::add_or_update_section( $section_title, $section_content ); 48 | $this->assertStringNotContainsString( $section_title, $readme, 'The title of an empty section should not be added.' ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/CbtThemeReadme/base.php: -------------------------------------------------------------------------------- 1 | orig_active_theme_slug = get_option( 'stylesheet' ); 38 | 39 | // Create a test theme directory. 40 | $this->test_theme_dir = DIR_TESTDATA . '/themes/'; 41 | 42 | // Register test theme directory. 43 | register_theme_directory( $this->test_theme_dir ); 44 | 45 | // Switch to the test theme. 46 | switch_theme( 'test-theme-readme' ); 47 | 48 | // Store the original readme.txt content. 49 | $this->orig_readme_content = CBT_Theme_Readme::get_content(); 50 | } 51 | 52 | /** 53 | * Tears down tests. 54 | */ 55 | public function tear_down() { 56 | parent::tear_down(); 57 | 58 | // Restore the original readme.txt content. 59 | file_put_contents( CBT_Theme_Readme::file_path(), $this->orig_readme_content ); 60 | 61 | // Restore the original active theme. 62 | switch_theme( $this->orig_active_theme_slug ); 63 | } 64 | 65 | /** 66 | * Removes the newlines from a string. 67 | * 68 | * This is useful to make it easier to search for strings in the readme content. 69 | * Removes both DOS and Unix newlines. 70 | * 71 | * @param string $string 72 | * @return string 73 | */ 74 | public function remove_newlines( $string ) { 75 | return str_replace( array( "\r\n", "\n" ), '', $string ); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /tests/CbtThemeReadme/filePath.php: -------------------------------------------------------------------------------- 1 | assertEquals( $expected, $result ); 17 | 18 | $this->assertEquals( 'test-theme-readme', get_option( 'stylesheet' ) ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/CbtThemeReadme/getContent.php: -------------------------------------------------------------------------------- 1 | assertEquals( $expected, $result ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/CbtThemeReadme/update.php: -------------------------------------------------------------------------------- 1 | assertStringNotContainsString( "\r\n", $readme, 'The readme content contains DOS newlines.' ); 23 | 24 | // Removes the newlines from the readme content to make it easier to search for strings. 25 | $readme_without_newlines = $this->remove_newlines( $readme ); 26 | 27 | $expected_author = 'Contributors: ' . $data['author']; 28 | $expected_wp_version = 'Tested up to: ' . $data['wp_version'] ?? CBT_Theme_Utils::get_current_wordpress_version(); 29 | $expected_image_credits = '== Images ==' . $this->remove_newlines( $data['image_credits'] ); 30 | $expected_recommended_plugins = '== Recommended Plugins ==' . $this->remove_newlines( $data['recommended_plugins'] ); 31 | 32 | $this->assertStringContainsString( $expected_author, $readme_without_newlines, 'The expected author is missing.' ); 33 | $this->assertStringContainsString( $expected_wp_version, $readme_without_newlines, 'The expected WP version is missing.' ); 34 | $this->assertStringContainsString( $expected_image_credits, $readme_without_newlines, 'The expected image credits are missing.' ); 35 | $this->assertStringContainsString( $expected_recommended_plugins, $readme_without_newlines, 'The expected recommended plugins are missing.' ); 36 | 37 | // Assertion specific to font credits. 38 | if ( isset( $data['font_credits'] ) ) { 39 | $expected_font_credits = '== Fonts ==' . $this->remove_newlines( $data['font_credits'] ); 40 | $this->assertStringContainsString( $expected_font_credits, $readme_without_newlines, 'The expected font credits are missing.' ); 41 | } 42 | } 43 | 44 | public function data_test_update() { 45 | return array( 46 | 'complete data' => array( 47 | 'data' => array( 48 | 'description' => 'New theme description', 49 | 'author' => 'New theme author', 50 | 'wp_version' => '12.12', 51 | 'requires_wp' => '', 52 | 'image_credits' => 'New image credits', 53 | 'recommended_plugins' => 'New recommended plugins', 54 | 'font_credits' => 'Example font credits text', 55 | ), 56 | ), 57 | 'missing font credits' => array( 58 | 'data' => array( 59 | 'description' => 'New theme description', 60 | 'author' => 'New theme author', 61 | 'wp_version' => '12.12', 62 | 'requires_wp' => '', 63 | 'image_credits' => 'New image credits', 64 | 'recommended_plugins' => 'New recommended plugins', 65 | ), 66 | ), 67 | /* 68 | * This string contains DOS newlines. 69 | * It uses double quotes to make PHP interpret the newlines as newlines and not as string literals. 70 | */ 71 | 'Remove DOS newlines' => array( 72 | 'data' => array( 73 | 'description' => 'New theme description', 74 | 'author' => 'New theme author', 75 | 'wp_version' => '12.12', 76 | 'requires_wp' => '', 77 | 'image_credits' => "New image credits \r\n New image credits 2", 78 | 'recommended_plugins' => "Plugin1 \r\n Plugin2 \r\n Plugin3", 79 | 'font_credits' => "Font1 \r\n Font2 \r\n Font3", 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | content = ' 10 | 11 |
Alternative Text
12 | 13 | '; 14 | $new_template = CBT_Theme_Media::make_template_images_local( $template ); 15 | 16 | // The image should be replaced with a relative URL 17 | $this->assertStringNotContainsString( 'http://example.com/image.jpg', $new_template->content ); 18 | $this->assertStringContainsString( 'get_template_directory_uri', $new_template->content ); 19 | $this->assertStringContainsString( '/assets/images', $new_template->content ); 20 | 21 | } 22 | 23 | public function test_make_cover_block_local() { 24 | $template = new stdClass(); 25 | $template->content = ' 26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 | '; 34 | $new_template = CBT_Theme_Media::make_template_images_local( $template ); 35 | 36 | // The image should be replaced with a relative URL 37 | $this->assertStringNotContainsString( 'http://example.com/image.jpg', $new_template->content ); 38 | $this->assertStringContainsString( 'get_template_directory_uri', $new_template->content ); 39 | $this->assertStringContainsString( '/assets/images', $new_template->content ); 40 | } 41 | 42 | public function test_template_with_media_correctly_prepared() { 43 | $template = new stdClass(); 44 | $template->slug = 'test-template'; 45 | $template->content = ' 46 | 47 |
Alternative Text
48 | 49 | '; 50 | $new_template = CBT_Theme_Templates::prepare_template_for_export( $template ); 51 | 52 | // Content should be replaced with a pattern block 53 | $this->assertStringContainsString( ' 68 |
69 | 70 | '; 71 | $new_template = CBT_Theme_Templates::prepare_template_for_export( $template ); 72 | 73 | // Content should be replaced with a pattern block 74 | $this->assertStringContainsString( ' 16 | '; 17 | 18 | $updated_pattern_string = CBT_Theme_Utils::replace_namespace( $pattern_string, 'old-slug', 'new-slug', 'Old Name', 'New Name' ); 19 | $this->assertStringContainsString( 'Slug: new-slug/index', $updated_pattern_string ); 20 | $this->assertStringNotContainsString( 'old-slug', $updated_pattern_string ); 21 | 22 | } 23 | 24 | public function test_replace_namespace_in_code() { 25 | $code_string = "assertStringContainsString( '@package new-slug', $updated_code_string ); 40 | $this->assertStringNotContainsString( 'old-slug', $updated_code_string ); 41 | $this->assertStringContainsString( 'function new_slug_support', $updated_code_string ); 42 | $this->assertStringContainsString( "function_exists( 'new_slug_support' )", $updated_code_string ); 43 | } 44 | 45 | public function test_replace_namespace_in_code_with_single_word_slug() { 46 | $code_string = "assertStringContainsString( '@package new-slug', $updated_code_string ); 61 | $this->assertStringNotContainsString( 'old-slug', $updated_code_string ); 62 | $this->assertStringContainsString( 'function new_slug_support', $updated_code_string ); 63 | $this->assertStringContainsString( "function_exists( 'new_slug_support' )", $updated_code_string ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /update-version-and-changelog.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | const fs = require( 'fs' ); 7 | const core = require( '@actions/core' ); 8 | const simpleGit = require( 'simple-git' ); 9 | const { promisify } = require( 'util' ); 10 | const exec = promisify( require( 'child_process' ).exec ); 11 | 12 | const git = simpleGit.default(); 13 | 14 | const releaseType = process.env.RELEASE_TYPE; 15 | const VALID_RELEASE_TYPES = [ 'major', 'minor', 'patch' ]; 16 | 17 | // To get the merges since the last (previous) tag 18 | async function getChangesSinceLastTag() { 19 | try { 20 | // Fetch all tags, sorted by creation date 21 | const tagsResult = await git.tags( { 22 | '--sort': '-creatordate', 23 | } ); 24 | const tags = tagsResult.all; 25 | if ( tags.length === 0 ) { 26 | console.error( '❌ Error: No previous tags found.' ); 27 | return null; 28 | } 29 | const previousTag = tags[ 0 ]; // The most recent tag 30 | 31 | // Now get the changes since this tag 32 | const changes = await git.log( [ `${ previousTag }..HEAD` ] ); 33 | return changes; 34 | } catch ( error ) { 35 | throw error; 36 | } 37 | } 38 | 39 | // To know if there are changes since the last tag. 40 | // we are not using getChangesSinceGitTag because it returns the just the merges and not the commits. 41 | // So for example if a hotfix was committed directly to trunk this function will detect it but getChangesSinceGitTag will not. 42 | async function getHasChangesSinceGitTag( tag ) { 43 | const changes = await git.log( [ `HEAD...${ tag }` ] ); 44 | return changes?.all?.length > 0; 45 | } 46 | 47 | async function updateVersion() { 48 | if ( ! VALID_RELEASE_TYPES.includes( releaseType ) ) { 49 | console.error( 50 | '❌ Error: Release type is not valid. Valid release types are: major, minor, patch.' 51 | ); 52 | process.exit( 1 ); 53 | } 54 | 55 | if ( 56 | ! fs.existsSync( './package.json' ) || 57 | ! fs.existsSync( './package-lock.json' ) 58 | ) { 59 | console.error( '❌ Error: package.json or lock file not found.' ); 60 | process.exit( 1 ); 61 | } 62 | 63 | if ( ! fs.existsSync( './readme.txt' ) ) { 64 | console.error( '❌ Error: readme.txt file not found.' ); 65 | process.exit( 1 ); 66 | } 67 | 68 | if ( ! fs.existsSync( './create-block-theme.php' ) ) { 69 | console.error( '❌ Error: create-block-theme.php file not found.' ); 70 | process.exit( 1 ); 71 | } 72 | 73 | // get changes since last tag 74 | let changes = []; 75 | try { 76 | changes = await getChangesSinceLastTag(); 77 | } catch ( error ) { 78 | console.error( 79 | `❌ Error: failed to get changes since last tag: ${ error }` 80 | ); 81 | process.exit( 1 ); 82 | } 83 | 84 | const packageJson = require( './package.json' ); 85 | const currentVersion = packageJson.version; 86 | 87 | // version bump package.json and package-lock.json using npm 88 | const { stdout, stderr } = await exec( 89 | `npm version --commit-hooks false --git-tag-version false ${ releaseType }` 90 | ); 91 | if ( stderr ) { 92 | console.error( `❌ Error: failed to bump the version."` ); 93 | process.exit( 1 ); 94 | } 95 | 96 | const currentTag = `v${ currentVersion }`; 97 | const newTag = stdout.trim(); 98 | const newVersion = newTag.replace( 'v', '' ); 99 | const hasChangesSinceGitTag = await getHasChangesSinceGitTag( currentTag ); 100 | 101 | // check if there are any changes 102 | if ( ! hasChangesSinceGitTag ) { 103 | console.error( 104 | `❌ No changes since last tag (${ currentTag }). There is nothing new to release.` 105 | ); 106 | // revert version update 107 | await exec( 108 | `npm version --commit-hooks false --git-tag-version false ${ currentVersion }` 109 | ); 110 | process.exit( 1 ); 111 | } 112 | 113 | console.info( '✅ Package.json version updated', currentTag, '=>', newTag ); 114 | 115 | // update readme.txt version with the new changelog 116 | const readme = fs.readFileSync( './readme.txt', 'utf8' ); 117 | const capitalizeFirstLetter = ( string ) => 118 | string.charAt( 0 ).toUpperCase() + string.slice( 1 ); 119 | 120 | const changelogChanges = changes.all 121 | .map( 122 | ( change ) => 123 | `* ${ capitalizeFirstLetter( change.message || change.body ) }` 124 | ) 125 | .join( '\n' ); 126 | const newChangelog = `== Changelog ==\n\n= ${ newVersion } =\n${ changelogChanges }`; 127 | let newReadme = readme.replace( '== Changelog ==', newChangelog ); 128 | // update version in readme.txt 129 | newReadme = newReadme.replace( 130 | /Stable tag: (.*)/, 131 | `Stable tag: ${ newVersion }` 132 | ); 133 | fs.writeFileSync( './readme.txt', newReadme ); 134 | console.info( '✅ Readme version updated', currentTag, '=>', newTag ); 135 | 136 | // update create-block-theme.php version 137 | const pluginPhpFile = fs.readFileSync( './create-block-theme.php', 'utf8' ); 138 | const newPluginPhpFile = pluginPhpFile.replace( 139 | /Version: (.*)/, 140 | `Version: ${ newVersion }` 141 | ); 142 | fs.writeFileSync( './create-block-theme.php', newPluginPhpFile ); 143 | console.info( 144 | '✅ create-block-theme.php file version updated', 145 | currentTag, 146 | '=>', 147 | newTag 148 | ); 149 | 150 | // output data to be used by the next steps of the github action 151 | core.setOutput( 'NEW_VERSION', newVersion ); 152 | core.setOutput( 'NEW_TAG', newTag ); 153 | core.setOutput( 'CHANGELOG', changelogChanges ); 154 | } 155 | 156 | updateVersion(); 157 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | const defaultConfig = require( '@wordpress/scripts/config/webpack.config.js' ); 5 | 6 | module.exports = { 7 | // Default wordpress config 8 | ...defaultConfig, 9 | 10 | // custom config to avoid errors with lib-font dependency 11 | ...{ 12 | resolve: { 13 | fallback: { 14 | zlib: false, 15 | fs: false, 16 | }, 17 | }, 18 | }, 19 | }; 20 | --------------------------------------------------------------------------------