├── .distignore ├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build-production-zip.yml │ ├── cypress-main.yml │ ├── cypress-push.yml │ ├── release-to-wp-org.yml │ └── update-wordpress-readme.yml ├── .gitignore ├── .prettierrc.js ├── .wordpress-org ├── banner-1544x500.png ├── banner-772x250.png ├── icon-128x128.png ├── icon-256x256.png ├── screenshot-1.png ├── screenshot-2.gif ├── screenshot-3.png ├── screenshot-4.png └── screenshot-5.png ├── .wp-env.json ├── README.md ├── code-block-pro.php ├── cypress.config.ts ├── cypress ├── constants.js ├── e2e │ ├── buttons.cy.js │ ├── compatability.cy.js │ ├── extra-settings.cy.js │ ├── footers.cy.js │ ├── headers.cy.js │ ├── height.cy.js │ ├── language.cy.js │ ├── lines.cy.js │ ├── misc.cy.js │ ├── padding.cy.js │ ├── permissions.cy.js │ ├── rtl.cy.js │ ├── styling.cy.js │ └── themes.cy.js └── support │ ├── commands.js │ ├── e2e.js │ ├── features │ ├── code.js │ ├── footers.js │ ├── headers.js │ ├── height.js │ ├── language.js │ └── theme.js │ ├── gutenberg.js │ ├── helpers.js │ ├── login-logout.js │ ├── navigate-pages.js │ ├── plugins.js │ └── wp-cli.js ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── php ├── compatibility.php ├── router.php └── routes.php ├── postcss.config.js ├── readme.txt ├── src ├── Editor.tsx ├── block.json ├── defaultLanguages.json ├── defaultThemes.json ├── editor │ ├── Edit.tsx │ ├── components │ │ ├── BlockFilter.tsx │ │ ├── ButtonsPanel.tsx │ │ ├── FontSelect.tsx │ │ ├── FooterSelect.tsx │ │ ├── HeaderSelect.tsx │ │ ├── HeightPanel.tsx │ │ ├── SeeMoreSelect.tsx │ │ ├── SlotFactory.tsx │ │ ├── ThemeFilter.tsx │ │ ├── ThemePreview.tsx │ │ ├── ThemeSelect.tsx │ │ ├── ThemesPanel.tsx │ │ ├── buttons │ │ │ ├── ButtonList.tsx │ │ │ ├── copy │ │ │ │ ├── Clipboard.tsx │ │ │ │ ├── CopyButton.tsx │ │ │ │ ├── TextSimple.tsx │ │ │ │ └── TwoSquares.tsx │ │ │ └── sidebar │ │ │ │ └── CopyBtnSettings.tsx │ │ ├── footers │ │ │ ├── SimpleStringEnd.tsx │ │ │ └── SimpleStringStart.tsx │ │ ├── headers │ │ │ ├── Headlights.tsx │ │ │ ├── HeadlightsMuted.tsx │ │ │ ├── HeadlightsMutedAlt.tsx │ │ │ ├── PillString.tsx │ │ │ ├── SimpleString.tsx │ │ │ └── StringSmall.tsx │ │ ├── misc │ │ │ ├── MissingPermissions.tsx │ │ │ └── Unsupported.tsx │ │ └── seemore │ │ │ ├── BlockLeft.tsx │ │ │ ├── BlockRight.tsx │ │ │ └── RoundCenter.tsx │ ├── controls │ │ ├── BlurControl.tsx │ │ ├── HighlightingControl.tsx │ │ ├── Sidebar.tsx │ │ └── Toolbar.tsx │ ├── editor.css │ └── transforms.ts ├── fonts │ ├── Code-Pro-Comic-Mono.ttf │ ├── Code-Pro-Cozette.woff2 │ ├── Code-Pro-Deja-Vu-Mono.ttf │ ├── Code-Pro-Fantasque-Sans-Mono.woff2 │ ├── Code-Pro-Fira-Code.woff2 │ ├── Code-Pro-Geist-Mono.woff2 │ ├── Code-Pro-Hack.woff2 │ ├── Code-Pro-JetBrains-Mono-NL.ttf │ ├── Code-Pro-JetBrains-Mono.woff2 │ ├── Code-Pro-Monaspace-Argon.woff │ ├── Code-Pro-Monaspace-Krypton.woff │ ├── Code-Pro-Monaspace-Neon.woff │ ├── Code-Pro-Monaspace-Radon.woff │ ├── Code-Pro-Monaspace-Xenon.woff │ ├── Code-Pro-Roboto-Mono.ttf │ └── licenses │ │ ├── Comic Mono.txt │ │ ├── Cozette.txt │ │ ├── Deja-Vu.txt │ │ ├── Fantasque.txt │ │ ├── Fira Code.txt │ │ ├── Geist.txt │ │ ├── Hack.txt │ │ ├── JetBrains.txt │ │ ├── Monaspace.txt │ │ └── Roboto Mono.txt ├── front │ ├── BlockOutput.tsx │ ├── front.js │ └── style.css ├── hooks │ ├── useCanEditHTML.ts │ ├── useDefaults.ts │ ├── useLanguage.ts │ └── useTheme.ts ├── icons.tsx ├── index.tsx ├── state │ ├── global.ts │ ├── language.ts │ ├── settings.ts │ └── theme.ts ├── types.ts └── util │ ├── arrayHelpers.ts │ ├── code.ts │ ├── colors.ts │ ├── fonts.ts │ ├── languages.ts │ └── themes.ts ├── tailwind.config.js ├── tsconfig.json └── webpack.config.js /.distignore: -------------------------------------------------------------------------------- 1 | /.wordpress-org 2 | /.git 3 | /.github 4 | /.gitignore 5 | /node_modules 6 | /assets 7 | /scripts 8 | /package-lock.json 9 | /target 10 | /cypress 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | tabSize = 4 10 | trim_trailing_whitespace = true 11 | 12 | [*.yml] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: [kevinbatdorf] 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'monthly' 7 | -------------------------------------------------------------------------------- /.github/workflows/build-production-zip.yml: -------------------------------------------------------------------------------- 1 | name: Build production zip file 2 | on: 3 | push: 4 | workflow_dispatch: 5 | jobs: 6 | build: 7 | name: Build zip file 8 | concurrency: 9 | group: production - ${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v4 15 | - name: npm install and build 16 | run: | 17 | npm ci 18 | npx eslint --max-warnings 0 . 19 | npm run build 20 | env: 21 | CI: true 22 | 23 | - name: Package 24 | uses: actions/upload-artifact@v4 25 | with: 26 | name: code-block-pro 27 | retention-days: 5 28 | path: | 29 | ${{ github.workspace }}/ 30 | !${{ github.workspace }}/node_modules/ 31 | !${{ github.workspace }}/cypress/ 32 | !${{ github.workspace }}/target/ 33 | !${{ github.workspace }}/scripts/ 34 | !${{ github.workspace }}/.git/ 35 | !${{ github.workspace }}/.github/ 36 | !${{ github.workspace }}/.wordpress-org/ 37 | !${{ github.workspace }}/assets/ 38 | !${{ github.workspace }}/scripts/ 39 | !${{ github.workspace }}/package-lock.json 40 | -------------------------------------------------------------------------------- /.github/workflows/cypress-main.yml: -------------------------------------------------------------------------------- 1 | name: Run Cypress on main 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | schedule: 8 | # At 08:00 daily 9 | - cron: '0 8 * * *' 10 | jobs: 11 | generate_file_list: 12 | name: Generate file list 13 | runs-on: ubuntu-latest 14 | outputs: 15 | spec: ${{ steps.list_files.outputs.spec }} 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | - name: List Files 20 | id: list_files 21 | run: | 22 | # List all the .cy.js files in the cypress/e2e directory 23 | # and output their file names without the extension 24 | ls cypress/e2e/*.cy.js | xargs -I{} basename {} .cy.js | jq -Rrs "split(\"\n\") | map(select(length > 0)) | tojson" | tee /tmp/spec.json 25 | echo "spec=$(cat /tmp/spec.json)" >> "$GITHUB_OUTPUT" 26 | 27 | npm_install: 28 | name: Install Node modules 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v4 33 | - name: Cache node modules 34 | uses: actions/cache@v4 35 | id: cache-node-modules 36 | with: 37 | path: node_modules 38 | key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }} 39 | - name: Install npm dependencies 40 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 41 | run: npm ci 42 | 43 | test: 44 | name: WordPress ${{ matrix.wp-version }} (${{ matrix.spec }}) 45 | concurrency: 46 | group: cypress - ${{ github.event.pull_request.number || github.ref }} - ${{ matrix.wp-version }} - ${{ matrix.spec }} 47 | cancel-in-progress: true 48 | runs-on: ubuntu-latest 49 | needs: [npm_install, generate_file_list] 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | spec: ${{ fromJson(needs.generate_file_list.outputs.spec) }} 54 | wp-version: [null, 'Next'] 55 | steps: 56 | - name: Clone repo 57 | uses: actions/checkout@v4 58 | - name: Set up node 59 | uses: actions/setup-node@v3 60 | - name: Node modules cache 61 | uses: actions/cache@v4 62 | id: cache-node-modules 63 | with: 64 | path: node_modules 65 | key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }} 66 | - name: Install npm dependencies 67 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 68 | run: npm ci 69 | - name: Build 70 | run: | 71 | npx eslint --max-warnings 0 . 72 | npm run build 73 | 74 | - name: Get Latest WP Branch 75 | if: ${{ matrix.wp-version }} 76 | run: | 77 | echo "branch=$(curl -L -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/WordPress/WordPress/branches\?per_page\=100 | jq '.[].name' | awk '/-branch/' | sort -V | tail -n1 | tr -d '"')" >> $GITHUB_ENV 78 | - name: Maybe remove .wp-env.json 79 | if: ${{ matrix.wp-version }} 80 | run: rm .wp-env.json 81 | - name: Maybe change WP version 82 | uses: jsdaniell/create-json@v1.2.3 83 | if: ${{ matrix.wp-version }} 84 | with: 85 | name: '.wp-env.json' 86 | json: '{"core": "WordPress/WordPress#${{ env.branch }}","plugins":["."],"mappings":{"wp-content/plugins/prismatic": "https://downloads.wordpress.org/plugin/prismatic.zip"}}' 87 | 88 | - name: Start wp-env 89 | uses: nick-fields/retry@v3 90 | with: 91 | timeout_minutes: 4 92 | max_attempts: 3 93 | retry_wait_seconds: 60 94 | shell: bash 95 | command: | 96 | npx wp-env start --update 97 | echo "WordPress version: `npx wp-env run cli core version`" 98 | 99 | - name: Cypress run 100 | uses: cypress-io/github-action@v6 101 | with: 102 | spec: cypress/e2e/${{ matrix.spec }}.cy.js 103 | env: 104 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 105 | - uses: actions/upload-artifact@v4 106 | if: failure() 107 | with: 108 | name: cypress-screenshots 109 | path: cypress/screenshots 110 | - uses: actions/upload-artifact@v4 111 | if: failure() 112 | with: 113 | name: cypress-videos 114 | path: cypress/videos 115 | -------------------------------------------------------------------------------- /.github/workflows/cypress-push.yml: -------------------------------------------------------------------------------- 1 | name: Run Cypress on push 2 | on: 3 | push: 4 | branches-ignore: 5 | - main 6 | workflow_dispatch: 7 | jobs: 8 | generate_file_list: 9 | name: Generate file list 10 | runs-on: ubuntu-latest 11 | outputs: 12 | spec: ${{ steps.list_files.outputs.spec }} 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | - name: List Files 17 | id: list_files 18 | run: | 19 | # List all the .cy.js files in the cypress/e2e directory 20 | # and output their file names without the extension 21 | ls cypress/e2e/*.cy.js | xargs -I{} basename {} .cy.js | jq -Rrs "split(\"\n\") | map(select(length > 0)) | tojson" | tee /tmp/spec.json 22 | echo "spec=$(cat /tmp/spec.json)" >> "$GITHUB_OUTPUT" 23 | npm_install: 24 | name: Install Node modules 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | - name: Cache node modules 30 | uses: actions/cache@v4 31 | id: cache-node-modules 32 | with: 33 | path: node_modules 34 | key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }} 35 | - name: Install npm dependencies 36 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 37 | run: npm ci 38 | 39 | test: 40 | name: WordPress (${{ matrix.spec }}) 41 | concurrency: 42 | group: cypress - ${{ github.event.pull_request.number || github.ref }} - ${{ matrix.spec }} 43 | cancel-in-progress: true 44 | runs-on: ubuntu-latest 45 | needs: [npm_install, generate_file_list] 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | spec: ${{ fromJson(needs.generate_file_list.outputs.spec) }} 50 | steps: 51 | - name: Checkout code 52 | uses: actions/checkout@v4 53 | - name: Restore node modules cache 54 | uses: actions/cache@v4 55 | with: 56 | path: node_modules 57 | key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }} 58 | restore-keys: ${{ runner.os }}-node-modules- 59 | - name: Build 60 | run: | 61 | npx eslint --max-warnings 0 . 62 | npm run build 63 | 64 | - name: Start wp-env 65 | uses: nick-fields/retry@v3 66 | with: 67 | timeout_minutes: 4 68 | max_attempts: 3 69 | retry_wait_seconds: 60 70 | shell: bash 71 | command: | 72 | npx wp-env start --update 73 | echo "WordPress version: `npx wp-env run cli core version`" 74 | 75 | - name: Cypress run 76 | uses: cypress-io/github-action@v6 77 | with: 78 | spec: cypress/e2e/${{ matrix.spec }}.cy.js 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | - uses: actions/upload-artifact@v4 82 | if: failure() 83 | with: 84 | name: cypress-screenshots 85 | path: cypress/screenshots 86 | overwrite: true 87 | - uses: actions/upload-artifact@v4 88 | if: failure() 89 | with: 90 | name: cypress-videos 91 | path: cypress/videos 92 | overwrite: true 93 | -------------------------------------------------------------------------------- /.github/workflows/release-to-wp-org.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to WordPress.org 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | tag: 7 | name: Make release 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | 13 | - name: npm install and build 14 | run: | 15 | npm ci 16 | npx eslint --max-warnings 0 . 17 | npm run build 18 | env: 19 | CI: true 20 | 21 | - name: WordPress Plugin Deploy 22 | id: deploy 23 | uses: 10up/action-wordpress-plugin-deploy@stable 24 | with: 25 | generate-zip: true 26 | env: 27 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 28 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 29 | SLUG: code-block-pro 30 | -------------------------------------------------------------------------------- /.github/workflows/update-wordpress-readme.yml: -------------------------------------------------------------------------------- 1 | name: Plugin asset/readme update 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | trunk: 8 | name: Push to WordPress.org 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: npm install and build 13 | run: | 14 | npm ci 15 | npm run build 16 | env: 17 | CI: true 18 | - name: WordPress.org plugin asset/readme update 19 | uses: 10up/action-wordpress-plugin-asset-update@stable 20 | continue-on-error: true 21 | env: 22 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 23 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 24 | SLUG: code-block-pro 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /target 4 | Cargo.lock 5 | 6 | cypress/screenshots/**/* 7 | !cypress/screenshots/**/.gitkeep 8 | cypress/videos/**/* 9 | !cypress/videos/**/.gitkeep 10 | cypress/downloads/**/* 11 | cypress/downloads/**/.gitkeep 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | trailingComma: 'all', 4 | tabWidth: 4, 5 | semi: true, 6 | singleQuote: true, 7 | bracketSameLine: true, 8 | plugins: ['@trivago/prettier-plugin-sort-imports'], 9 | importOrder: ['^@wordpress/(.*)$', '', '^[./]'], 10 | overrides: [ 11 | { 12 | files: ['**/*.html'], 13 | options: { 14 | singleQuote: false, 15 | }, 16 | }, 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /.wordpress-org/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinBatdorf/code-block-pro/1276a0fbc0f0d5a3be2af3f649b1fb7d916f81ab/.wordpress-org/banner-1544x500.png -------------------------------------------------------------------------------- /.wordpress-org/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinBatdorf/code-block-pro/1276a0fbc0f0d5a3be2af3f649b1fb7d916f81ab/.wordpress-org/banner-772x250.png -------------------------------------------------------------------------------- /.wordpress-org/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinBatdorf/code-block-pro/1276a0fbc0f0d5a3be2af3f649b1fb7d916f81ab/.wordpress-org/icon-128x128.png -------------------------------------------------------------------------------- /.wordpress-org/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinBatdorf/code-block-pro/1276a0fbc0f0d5a3be2af3f649b1fb7d916f81ab/.wordpress-org/icon-256x256.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinBatdorf/code-block-pro/1276a0fbc0f0d5a3be2af3f649b1fb7d916f81ab/.wordpress-org/screenshot-1.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinBatdorf/code-block-pro/1276a0fbc0f0d5a3be2af3f649b1fb7d916f81ab/.wordpress-org/screenshot-2.gif -------------------------------------------------------------------------------- /.wordpress-org/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinBatdorf/code-block-pro/1276a0fbc0f0d5a3be2af3f649b1fb7d916f81ab/.wordpress-org/screenshot-3.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinBatdorf/code-block-pro/1276a0fbc0f0d5a3be2af3f649b1fb7d916f81ab/.wordpress-org/screenshot-4.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinBatdorf/code-block-pro/1276a0fbc0f0d5a3be2af3f649b1fb7d916f81ab/.wordpress-org/screenshot-5.png -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": null, 3 | "plugins": ["."] 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Code highlighting powered by the VS Code engine 2 | 3 | Read more at [code-block-pro.com](https://code-block-pro.com?utm_campaign=github&utm_source=gh-readme&utm_medium=textlink) 4 | 5 | View all themes at [code-block-pro.com/themes](https://code-block-pro.com/themes?utm_campaign=themes&utm_source=gh-readme&utm_medium=textlink) 6 | 7 | View this block plugin [on WordPress.org](https://wordpress.org/plugins/code-block-pro) 8 | 9 | [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/kevinbatdorf.svg?style=social&label=Follow%20%40kevinbatdorf)](https://twitter.com/kevinbatdorf) 10 | 11 | ## Features 12 | 13 | - Includes 25+ built-in themes to choose from. 14 | - Supports over 140 programming languages 15 | - Optionally load programming fonts 16 | - Line numbers 17 | - Line highlighting (static and on hover) 18 | - Blur highlighting (with reveal on hover) 19 | - Header styles 20 | - Footer styles 21 | - Optionally add a copy button to let users copy the code 22 | - Native Gutenberg block output - no special requirements 23 | - Core functionality works in headless mode (see FAQ) 24 | - Supports converting from the default code block 25 | 26 | ## Tips 27 | 28 | - Try combining line highlighting with the blur effect to add some extra depth 29 | - All settings are per block, but some settings are remembered when you add the next block. 30 | - Add a link in the code footer (some footers support this, not all) that points to a https://codepen.io demo. 31 | 32 | ## Example Screenshots 33 | 34 | ![alt text](.wordpress-org/screenshot-3.png 'Example 3') 35 | ![alt text](.wordpress-org/screenshot-4.png 'Example 4') 36 | ![alt text](.wordpress-org/screenshot-2.gif 'Example 2') 37 | -------------------------------------------------------------------------------- /code-block-pro.php: -------------------------------------------------------------------------------- 1 | esc_url_raw(plugin_dir_url(__FILE__)), 26 | ]) . ';'); 27 | }); 28 | 29 | add_action('admin_init', function () { 30 | wp_add_inline_script('kevinbatdorf-code-block-pro-editor-script', 'window.codeBlockPro = ' . wp_json_encode([ 31 | 'pluginUrl' => esc_url_raw(plugin_dir_url(__FILE__)), 32 | ]) . ';'); 33 | }); 34 | 35 | include_once(__DIR__ . '/php/compatibility.php'); 36 | include_once(__DIR__ . '/php/router.php'); 37 | include_once(__DIR__ . '/php/routes.php'); 38 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:8888', 6 | defaultCommandTimeout: 10_000, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /cypress/constants.js: -------------------------------------------------------------------------------- 1 | export const MORE_THEMES_URL = 2 | 'https://code-block-pro.com/themes?utm_campaign=notice'; 3 | -------------------------------------------------------------------------------- /cypress/e2e/compatability.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.loginUser(); 4 | }); 5 | afterEach(() => { 6 | // make sure we can uninstall prismatic 7 | cy.uninstallPlugin('prismatic'); 8 | cy.logoutUser(); 9 | }); 10 | context('Compatability checks', () => { 11 | it('Installs alongside Prismatic with no errors', () => { 12 | cy.installPlugin('prismatic'); 13 | // make sure we don't see thhe word fatal or error 14 | cy.get('body').should('not.contain', 'fatal error'); 15 | cy.visitAdminPage('plugins.php'); 16 | // make sure both prismatic and code-block-pro are active 17 | cy.get('#deactivate-code-block-pro').should('exist'); 18 | cy.get('#deactivate-prismatic').should('exist'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /cypress/e2e/extra-settings.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.clearBrowserStorage(); 4 | cy.loginUser(); 5 | cy.visitNewPageEditor(); 6 | cy.addBlock('kevinbatdorf/code-block-pro'); 7 | cy.findBlock('code-block-pro').should('exist'); 8 | 9 | cy.focusBlock( 10 | 'code-block-pro', 11 | 'textarea.npm__react-simple-code-editor__textarea', 12 | ); 13 | cy.findBlock( 14 | 'code-block-pro', 15 | 'textarea.npm__react-simple-code-editor__textarea', 16 | ).should('have.focus'); 17 | }); 18 | afterEach(() => { 19 | cy.saveDraft(); // so we can leave without an alert 20 | cy.logoutUser(); 21 | }); 22 | context('Extra Settings', () => { 23 | it('Disallows <', () => { 24 | cy.openSideBarPanel('Extra Settings'); 25 | 26 | cy.get('[data-cy="use-decode-uri"]') 27 | .should('exist') 28 | .should('not.be.checked'); 29 | 30 | cy.addCode('', { 31 | codeOutput: '', 32 | }); 33 | 34 | cy.findBlock('code-block-pro', 'pre') 35 | .invoke('html') 36 | .should('not.contain', encodeURI('<')); 37 | 38 | cy.previewCurrentPage(); 39 | 40 | cy.get('.wp-block-kevinbatdorf-code-block-pro pre') 41 | .should('exist') 42 | .should('contain', ''); 43 | }); 44 | 45 | it('Allows < when checked', () => { 46 | cy.openSideBarPanel('Extra Settings'); 47 | 48 | cy.get('[data-cy="use-decode-uri"]').check(); 49 | 50 | cy.addCode(''); 51 | 52 | cy.findBlock('code-block-pro', 'pre') 53 | .invoke('html') 54 | .should('contain', '>lt'); // formatted with highlighter 55 | 56 | cy.previewCurrentPage(); 57 | 58 | cy.get('.wp-block-kevinbatdorf-code-block-pro pre') 59 | .should('exist') 60 | .should('contain', ''); 61 | }); 62 | 63 | it('Escapes WordPress shortcodes', () => { 64 | cy.openSideBarPanel('Extra Settings'); 65 | cy.get('[data-cy="use-escape-shortcodes"]') 66 | .should('exist') 67 | .should('not.be.checked'); 68 | 69 | cy.setLanguage('plaintext'); 70 | cy.addCode('[embed]foo[/embed]'); 71 | cy.findBlock('code-block-pro', 'pre') 72 | .invoke('html') 73 | .should('contain', '[embed]foo[/embed]'); // Doesn't render 74 | 75 | cy.previewCurrentPage(); 76 | cy.get('.wp-block-kevinbatdorf-code-block-pro pre') 77 | .should('exist') 78 | .invoke('html') 79 | .should('contain', 'foo'); // Renders 80 | 81 | cy.go('back'); 82 | cy.focusBlock('code-block-pro'); 83 | cy.openSideBarPanel('Extra Settings'); 84 | cy.get('[data-cy="use-escape-shortcodes"]').check(); 85 | cy.findBlock('code-block-pro', 'pre') 86 | .invoke('html') 87 | .should('contain', '[embed]foo[/embed]'); // Doesn't render 88 | 89 | cy.previewCurrentPage(); 90 | cy.get('.wp-block-kevinbatdorf-code-block-pro pre') 91 | .should('exist') 92 | .invoke('html') 93 | .should('contain', '[embed]foo[/embed]'); // Doesn't render 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /cypress/e2e/footers.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.loginUser(); 4 | cy.visitNewPageEditor(); 5 | cy.addBlock('kevinbatdorf/code-block-pro'); 6 | cy.findBlock('code-block-pro').should('exist'); 7 | 8 | cy.focusBlock( 9 | 'code-block-pro', 10 | 'textarea.npm__react-simple-code-editor__textarea', 11 | ); 12 | cy.findBlock( 13 | 'code-block-pro', 14 | 'textarea.npm__react-simple-code-editor__textarea', 15 | ).should('have.focus'); 16 | }); 17 | afterEach(() => { 18 | cy.saveDraft(); // so we can leave without an alert 19 | cy.logoutUser(); 20 | }); 21 | context('Footers', () => { 22 | it('Renders no footer on insert and can swap', () => { 23 | cy.findBlock('code-block-pro') 24 | .find('div') 25 | .first() 26 | .siblings() 27 | .should('have.length', 2); 28 | cy.setFooter('simpleStringEnd'); 29 | cy.findBlock('code-block-pro') 30 | .find('div') 31 | .first() 32 | .siblings() 33 | .should('have.length', 3); 34 | cy.setFooter('none'); 35 | cy.findBlock('code-block-pro') 36 | .find('div') 37 | .first() 38 | .siblings() 39 | .should('have.length', 2); 40 | }); 41 | 42 | it('Can accept text inputs on some footers', () => { 43 | // Remove the header/footer if there 44 | cy.setHeader('none'); 45 | cy.setFooter('none'); 46 | cy.findBlock('code-block-pro') 47 | .invoke('html') 48 | .should('not.contain', 'JavaScript'); 49 | cy.setFooter('simpleStringEnd'); 50 | cy.findBlock('code-block-pro') 51 | .invoke('html') 52 | .should('contain', 'JavaScript'); 53 | cy.setLanguage('ruby'); 54 | cy.findBlock('code-block-pro').invoke('html').should('contain', 'Ruby'); 55 | cy.openSideBarPanel('Settings'); 56 | cy.get('#code-block-pro-footer-text') 57 | .should('exist') 58 | .should('have.value', '') 59 | .type('foo-bar-baz-lets-go'); 60 | cy.findBlock('code-block-pro') 61 | .invoke('html') 62 | .should('contain', 'foo-bar-baz-lets-go') 63 | .should('not.contain', 'Ruby') 64 | .should('not.contain', 'JavaScript'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /cypress/e2e/headers.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.loginUser(); 4 | cy.visitNewPageEditor(); 5 | cy.addBlock('kevinbatdorf/code-block-pro'); 6 | cy.findBlock('code-block-pro').should('exist'); 7 | 8 | cy.focusBlock( 9 | 'code-block-pro', 10 | 'textarea.npm__react-simple-code-editor__textarea', 11 | ); 12 | cy.findBlock( 13 | 'code-block-pro', 14 | 'textarea.npm__react-simple-code-editor__textarea', 15 | ).should('have.focus'); 16 | }); 17 | afterEach(() => { 18 | cy.saveDraft(); // so we can leave without an alert 19 | cy.logoutUser(); 20 | }); 21 | context('Headers', () => { 22 | it('Renders default header and can switch', () => { 23 | cy.findBlock('code-block-pro') 24 | .invoke('html') 25 | .should('contain', 'fill="#FF5F56" stroke="#E0443E"'); 26 | cy.setHeader('headlightsMuted'); 27 | cy.findBlock('code-block-pro') 28 | .invoke('html') 29 | .should('not.contain', 'fill="#FF5F56" stroke="#E0443E"') 30 | .should('contain', 'fill="#d8dee933" stroke="#d8dee94d"'); 31 | }); 32 | it('Can accept text inputs on some headers', () => { 33 | cy.setHeader('simpleString'); 34 | cy.findBlock('code-block-pro') 35 | .invoke('html') 36 | .should('contain', 'JavaScript'); 37 | cy.setLanguage('ruby'); 38 | cy.findBlock('code-block-pro').invoke('html').should('contain', 'Ruby'); 39 | cy.openSideBarPanel('Settings'); 40 | cy.get('#code-block-pro-header-text') 41 | .should('exist') 42 | .should('have.value', '') 43 | .type('Hello WordPress'); 44 | cy.findBlock('code-block-pro') 45 | .invoke('html') 46 | .should('contain', 'Hello WordPress') 47 | .should('not.contain', 'Ruby') 48 | .should('not.contain', 'JavaScript'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /cypress/e2e/language.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.loginUser(); 4 | cy.visitNewPageEditor(); 5 | cy.addBlock('kevinbatdorf/code-block-pro'); 6 | cy.findBlock('code-block-pro').should('exist'); 7 | 8 | cy.focusBlock( 9 | 'code-block-pro', 10 | 'textarea.npm__react-simple-code-editor__textarea', 11 | ); 12 | cy.findBlock( 13 | 'code-block-pro', 14 | 'textarea.npm__react-simple-code-editor__textarea', 15 | ).should('have.focus'); 16 | }); 17 | afterEach(() => { 18 | cy.saveDraft(); // so we can leave without an alert 19 | cy.logoutUser(); 20 | }); 21 | context('Language checks', () => { 22 | it('Renders properly when switching languages', () => { 23 | cy.addCode('const foo = "bar";'); 24 | cy.setTheme('nord'); 25 | cy.findBlock('code-block-pro') 26 | .invoke('html') 27 | .should('contain', 'const'); 28 | 29 | // TODO - pro only 30 | // const lorem = 'Ut laboris anim culpa fugiat sit anim dolor cillum'; 31 | // cy.addCode(lorem); 32 | cy.setLanguage('plaintext'); 33 | cy.findBlock('code-block-pro') 34 | .invoke('html') 35 | .should( 36 | 'contain', 37 | 'padding: 16px 0px 16px 16px;">const foo = "bar";', 38 | ); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /cypress/e2e/misc.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.clearBrowserStorage(); 4 | cy.loginUser(); 5 | cy.visitNewPageEditor(); 6 | cy.addBlock('kevinbatdorf/code-block-pro'); 7 | cy.findBlock('code-block-pro').should('exist'); 8 | 9 | cy.focusBlock( 10 | 'code-block-pro', 11 | 'textarea.npm__react-simple-code-editor__textarea', 12 | ); 13 | cy.findBlock( 14 | 'code-block-pro', 15 | 'textarea.npm__react-simple-code-editor__textarea', 16 | ).should('have.focus'); 17 | }); 18 | afterEach(() => { 19 | cy.saveDraft(); // so we can leave without an alert 20 | cy.logoutUser(); 21 | }); 22 | context('Miscellaneous', () => { 23 | it('Persists settings', () => { 24 | cy.addCode('const foo = "bar";'); 25 | cy.setTheme('dracula'); 26 | cy.findBlock('code-block-pro', 'pre') 27 | .invoke('html') 28 | .should('contain', 'const'); 29 | 30 | cy.previewCurrentPage(); // to save and move on 31 | 32 | cy.visitNewPageEditor(); 33 | cy.addBlock('kevinbatdorf/code-block-pro'); 34 | cy.findBlock('code-block-pro').should('exist'); 35 | 36 | // confirm theme is persisted 37 | cy.addCode('const foo = "bar";'); 38 | cy.findBlock('code-block-pro', 'pre') 39 | .invoke('html') 40 | .should('contain', 'const'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /cypress/e2e/padding.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.clearBrowserStorage(); 4 | cy.loginUser(); 5 | cy.visitNewPageEditor(); 6 | cy.addBlock('kevinbatdorf/code-block-pro'); 7 | cy.findBlock('code-block-pro').should('exist'); 8 | 9 | cy.focusBlock( 10 | 'code-block-pro', 11 | 'textarea.npm__react-simple-code-editor__textarea', 12 | ); 13 | cy.findBlock( 14 | 'code-block-pro', 15 | 'textarea.npm__react-simple-code-editor__textarea', 16 | ).should('have.focus'); 17 | 18 | cy.openSideBarPanel('Extra Settings'); 19 | cy.get('[data-cy="disable-padding"') 20 | .should('exist') 21 | .should('be.not.checked'); 22 | }); 23 | afterEach(() => { 24 | cy.saveDraft(); // so we can leave without an alert 25 | cy.logoutUser(); 26 | }); 27 | context('Line numbers', () => { 28 | it('Line numbers disabled, padding enabled', () => { 29 | cy.findBlock('code-block-pro', 'pre').should( 30 | 'have.css', 31 | 'padding', 32 | '16px 0px 16px 16px', 33 | ); 34 | cy.findBlock( 35 | 'code-block-pro', 36 | 'textarea.npm__react-simple-code-editor__textarea', 37 | ).should('have.css', 'padding', '16px 0px 16px 16px'); 38 | 39 | cy.addCode('line 1\nline 2\nline 3'); 40 | cy.previewCurrentPage(); 41 | 42 | cy.get('.wp-block-kevinbatdorf-code-block-pro pre') 43 | .should('exist') 44 | .should('have.css', 'padding', '16px 0px 16px 16px'); 45 | }); 46 | 47 | it('Line numbers disabled, padding disabled', () => { 48 | cy.findBlock('code-block-pro', 'pre').should( 49 | 'have.css', 50 | 'padding', 51 | '16px 0px 16px 16px', 52 | ); 53 | cy.findBlock( 54 | 'code-block-pro', 55 | 'textarea.npm__react-simple-code-editor__textarea', 56 | ).should('have.css', 'padding', '16px 0px 16px 16px'); 57 | 58 | cy.addCode('line 1\nline 2\nline 3'); 59 | 60 | cy.get('[data-cy="disable-padding"').check(); 61 | cy.get('[data-cy="disable-padding"').should('be.checked'); 62 | 63 | cy.findBlock('code-block-pro', 'pre').should( 64 | 'have.css', 65 | 'padding', 66 | '0px', 67 | ); 68 | cy.findBlock( 69 | 'code-block-pro', 70 | 'textarea.npm__react-simple-code-editor__textarea', 71 | ).should('have.css', 'padding', '0px'); 72 | 73 | cy.previewCurrentPage(); 74 | 75 | cy.get('.wp-block-kevinbatdorf-code-block-pro pre') 76 | .should('exist') 77 | .should('have.css', 'padding', '0px'); 78 | }); 79 | 80 | it('Line numbers enabled, padding enabled', () => { 81 | cy.addCode('line 1\nline 2\nline 3'); 82 | 83 | cy.openSideBarPanel('Line Settings'); 84 | cy.get('[data-cy="show-line-numbers"]').should('exist'); 85 | cy.get('[data-cy="show-line-numbers"]').should('be.not.checked'); 86 | cy.get('[data-cy="show-line-numbers"]').check(); 87 | cy.get('[data-cy="show-line-numbers"]').should('be.checked'); 88 | 89 | cy.findBlock('code-block-pro').invoke('html').should( 90 | 'not.include', 91 | '0px 0px 0px 4', // more like 40.4375px but varies 92 | ); 93 | 94 | cy.previewCurrentPage(); 95 | 96 | cy.get('.wp-block-kevinbatdorf-code-block-pro pre') 97 | .should('exist') 98 | .should('have.css', 'padding', '16px 0px 16px 16px'); 99 | }); 100 | 101 | it('Line numbers enabled, padding disabled', () => { 102 | cy.get('[data-cy="disable-padding"').check(); 103 | cy.get('[data-cy="disable-padding"').should('be.checked'); 104 | cy.addCode('line 1\nline 2\nline 3'); 105 | 106 | cy.openSideBarPanel('Line Settings'); 107 | cy.get('[data-cy="show-line-numbers"]').should('exist'); 108 | cy.get('[data-cy="show-line-numbers"]').should('be.not.checked'); 109 | cy.get('[data-cy="show-line-numbers"]').check(); 110 | cy.get('[data-cy="show-line-numbers"]').should('be.checked'); 111 | 112 | cy.findBlock('code-block-pro').invoke('html').should( 113 | 'include', 114 | '0px 0px 0px 2', // more like 24.4375px but varies 115 | ); 116 | 117 | // Tests that the padding expands as the line number width grows 118 | cy.addCode('1\n2\n3\n4\n5\n6\n7\n8\n9\n10'); 119 | 120 | cy.findBlock('code-block-pro') 121 | .invoke('html') 122 | .should( 123 | 'not.include', 124 | '0px 0px 0px 2', // more like 32.8594px but varies 125 | ) 126 | .should( 127 | 'include', 128 | '0px 0px 0px 3', // more like 32.8594px but varies 129 | ); 130 | 131 | cy.previewCurrentPage(); 132 | 133 | cy.get('.wp-block-kevinbatdorf-code-block-pro pre') 134 | .should('exist') 135 | .should('have.css', 'padding', '0px'); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /cypress/e2e/permissions.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.clearBrowserStorage(); 4 | cy.intercept( 5 | 'GET', 6 | '/index.php?rest_route=%2Fcode-block-pro%2Fv1%2Fcan-save-html&_locale=user', 7 | { 8 | body: false, 9 | }, 10 | ).as('canSaveHtml'); 11 | cy.loginUser(); 12 | cy.visitNewPostEditor(); 13 | }); 14 | afterEach(() => { 15 | cy.saveDraft(); // so we can leave without an alert 16 | cy.logoutUser(); 17 | }); 18 | context('Permissions', () => { 19 | it('Warning shows when cap is missing', () => { 20 | cy.addBlock('kevinbatdorf/code-block-pro'); 21 | cy.wait('@canSaveHtml'); 22 | cy.findBlock('code-block-pro') 23 | .invoke('html') 24 | .should('contain', 'capability is required'); 25 | }); 26 | 27 | it('Prevents updating attributes', () => { 28 | cy.addBlock('kevinbatdorf/code-block-pro'); 29 | 30 | cy.wait('@canSaveHtml'); 31 | 32 | cy.get('[data-cy="show-line-numbers"]').should('not.exist'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /cypress/e2e/rtl.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.clearBrowserStorage(); 4 | cy.loginUser(); 5 | cy.visitNewPageEditor(); 6 | cy.addBlock('kevinbatdorf/code-block-pro'); 7 | cy.findBlock('code-block-pro').should('exist'); 8 | 9 | cy.focusBlock( 10 | 'code-block-pro', 11 | 'textarea.npm__react-simple-code-editor__textarea', 12 | ); 13 | cy.findBlock( 14 | 'code-block-pro', 15 | 'textarea.npm__react-simple-code-editor__textarea', 16 | ).should('have.focus'); 17 | }); 18 | afterEach(() => { 19 | cy.saveDraft(); // so we can leave without an alert 20 | cy.logoutUser(); 21 | }); 22 | context('RTL', () => { 23 | it('Renders code LTR', () => { 24 | cy.addCode('const foo = "bar";'); 25 | // check ltr 26 | cy.findBlock('code-block-pro').should('have.css', 'direction', 'ltr'); 27 | 28 | // update language 29 | cy.saveDraft(); 30 | cy.switchWpLang('fa_AF'); 31 | cy.reload(); 32 | 33 | // assert code is still ltr 34 | cy.findBlock('code-block-pro').should('have.css', 'direction', 'ltr'); 35 | 36 | // preview page and assert code is ltr 37 | cy.previewCurrentPage(); 38 | cy.get('.wp-block-kevinbatdorf-code-block-pro').should( 39 | 'have.css', 40 | 'direction', 41 | 'ltr', 42 | ); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /cypress/e2e/styling.cy.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.resetDatabase(); 3 | cy.loginUser(); 4 | cy.visitNewPageEditor(); 5 | cy.addBlock('kevinbatdorf/code-block-pro'); 6 | cy.findBlock('code-block-pro').should('exist'); 7 | 8 | cy.focusBlock( 9 | 'code-block-pro', 10 | 'textarea.npm__react-simple-code-editor__textarea', 11 | ); 12 | cy.findBlock( 13 | 'code-block-pro', 14 | 'textarea.npm__react-simple-code-editor__textarea', 15 | ).should('have.focus'); 16 | }); 17 | afterEach(() => { 18 | cy.saveDraft(); // so we can leave without an alert 19 | cy.logoutUser(); 20 | }); 21 | context('Styling', () => { 22 | it('Font size can be changed', () => { 23 | cy.openSideBarPanel('Styling'); 24 | 25 | cy.findBlock('code-block-pro') 26 | .invoke('attr', 'style') 27 | .should('contain', 'font-size: 0.875rem'); 28 | 29 | cy.get('[data-cy="font-size-select"] [aria-label="Small"]').should( 30 | 'have.attr', 31 | 'aria-checked', 32 | 'true', 33 | ); 34 | cy.get('[data-cy="font-size-select"] [aria-label="Normal"]').should( 35 | 'have.attr', 36 | 'aria-checked', 37 | 'false', 38 | ); 39 | cy.get('[data-cy="font-size-select"] [aria-label="Normal"]').click(); 40 | cy.get('[data-cy="font-size-select"] [aria-label="Normal"]').should( 41 | 'have.attr', 42 | 'aria-checked', 43 | 'true', 44 | ); 45 | 46 | cy.findBlock('code-block-pro') 47 | .invoke('attr', 'style') 48 | .should('contain', 'font-size: 1rem'); 49 | }); 50 | 51 | it('Line height can be changed', () => { 52 | cy.openSideBarPanel('Styling'); 53 | 54 | cy.findBlock('code-block-pro') 55 | .invoke('attr', 'style') 56 | .should('contain', 'line-height: 1.25rem'); 57 | 58 | cy.get( 59 | '[data-cy="font-line-height-select"] [aria-label="Tight"]', 60 | ).should('have.attr', 'aria-checked', 'true'); 61 | cy.get( 62 | '[data-cy="font-line-height-select"] [aria-label="Normal"]', 63 | ).should('have.attr', 'aria-checked', 'false'); 64 | cy.get( 65 | '[data-cy="font-line-height-select"] [aria-label="Normal"]', 66 | ).click(); 67 | cy.get( 68 | '[data-cy="font-line-height-select"] [aria-label="Normal"]', 69 | ).should('have.attr', 'aria-checked', 'true'); 70 | 71 | cy.findBlock('code-block-pro') 72 | .invoke('attr', 'style') 73 | .should('contain', 'line-height: 1.5rem'); 74 | }); 75 | 76 | it('Font family can be changed', () => { 77 | cy.addCode('const foo = "bar";'); 78 | cy.openSideBarPanel('Styling'); 79 | 80 | cy.findBlock('code-block-pro') 81 | .invoke('attr', 'style') 82 | .should('contain', 'Code-Pro-JetBrains-Mono'); 83 | 84 | cy.get('#code-block-pro-font-family').select('Fira Code'); 85 | cy.get('#code-block-pro-font-family').should( 86 | 'have.value', 87 | 'Code-Pro-Fira-Code', 88 | ); 89 | 90 | cy.previewCurrentPage(); 91 | 92 | cy.get('.wp-block-kevinbatdorf-code-block-pro') 93 | .invoke('attr', 'style') 94 | .should('contain', 'font-family:Code-Pro-Fira-Code'); 95 | 96 | cy.go('back'); 97 | 98 | cy.focusBlock( 99 | 'code-block-pro', 100 | 'textarea.npm__react-simple-code-editor__textarea', 101 | ); 102 | cy.findBlock( 103 | 'code-block-pro', 104 | 'textarea.npm__react-simple-code-editor__textarea', 105 | ).should('have.focus'); 106 | 107 | cy.openSideBarPanel('Styling'); 108 | 109 | cy.get('#code-block-pro-font-family').select('JetBrains Mono'); 110 | cy.get('#code-block-pro-font-family').should( 111 | 'have.value', 112 | 'Code-Pro-JetBrains-Mono', 113 | ); 114 | cy.get('#code-block-pro-font-family').select('System Default'); 115 | cy.get('#code-block-pro-font-family').should('have.value', ''); 116 | 117 | cy.previewCurrentPage(); 118 | 119 | cy.get('.wp-block-kevinbatdorf-code-block-pro') 120 | .invoke('attr', 'style') 121 | .should('not.contain', 'font-family'); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /cypress/e2e/themes.cy.js: -------------------------------------------------------------------------------- 1 | import themes from '../../src/defaultThemes.json'; 2 | import { MORE_THEMES_URL } from '../constants'; 3 | 4 | beforeEach(() => { 5 | cy.resetDatabase(); 6 | cy.loginUser(); 7 | cy.visitNewPageEditor(); 8 | cy.addBlock('kevinbatdorf/code-block-pro'); 9 | cy.findBlock('code-block-pro').should('exist'); 10 | 11 | cy.focusBlock( 12 | 'code-block-pro', 13 | 'textarea.npm__react-simple-code-editor__textarea', 14 | ); 15 | cy.findBlock( 16 | 'code-block-pro', 17 | 'textarea.npm__react-simple-code-editor__textarea', 18 | ).should('have.focus'); 19 | }); 20 | afterEach(() => { 21 | cy.saveDraft(); // so we can leave without an alert 22 | cy.logoutUser(); 23 | }); 24 | context('Theme checks', () => { 25 | it('Renders properly when switching themes', () => { 26 | // Nord is the default 27 | cy.addCode('const foo = "bar";'); 28 | cy.setTheme('nord'); 29 | cy.findBlock('code-block-pro', 'pre') 30 | .invoke('html') 31 | .should('contain', 'const'); 32 | 33 | cy.setTheme('dracula'); 34 | cy.findBlock('code-block-pro', 'pre') 35 | .invoke('html') 36 | .should('contain', 'const'); 37 | 38 | cy.setTheme('rose-pine-dawn'); 39 | cy.findBlock('code-block-pro', 'pre') 40 | .invoke('html') 41 | .should('contain', 'const'); 42 | 43 | cy.setTheme('poimandres'); 44 | cy.findBlock('code-block-pro') 45 | .invoke('html') 46 | .should('contain', 'const'); 47 | }); 48 | 49 | it('Themes can be disabled and hidden from view', () => { 50 | cy.openSideBarPanel('Theme'); 51 | cy.get('div[aria-label="Editor settings"] button') 52 | .contains('Theme') 53 | .parents('.editor-sidebar') 54 | .scrollTo('bottom'); 55 | cy.get('#code-block-pro-theme-nord').should('exist'); 56 | cy.get('[data-cy="manage-themes"]').should('exist').click(); 57 | cy.get('.components-modal__header-heading').should( 58 | 'have.text', 59 | 'Manage Themes', 60 | ); 61 | cy.get('#code-block-pro-theme-manager label').contains('Nord').click(); 62 | cy.get('.code-block-pro-editor [aria-label^="Close"]').click(); 63 | cy.get('#code-block-pro-theme-nord').should('not.exist'); 64 | }); 65 | 66 | it('Themes can be filtered via search', () => { 67 | cy.openSideBarPanel('Theme'); 68 | cy.get('#code-block-pro-theme-monokai').should('exist'); 69 | cy.get('#code-block-pro-search-themes').type('monokai'); 70 | cy.get('#code-block-pro-theme-monokai').should('exist'); 71 | cy.get('#code-block-pro-theme-dracula').should('not.exist'); 72 | }); 73 | 74 | it('Themes CTA shows twice in panel and once in modal', () => { 75 | cy.openSideBarPanel('Theme'); 76 | cy.get(`[href^="${MORE_THEMES_URL}"]`).should('have.length', 2); 77 | cy.get('[data-cy="manage-themes"]').should('exist').click(); 78 | cy.get(`[href^="${MORE_THEMES_URL}"]`).should('exist'); 79 | cy.get('.code-block-pro-editor [aria-label^="Close"]').click(); 80 | }); 81 | 82 | it('Theme CTA "more themes" link can be removed', () => { 83 | cy.window().then((win) => { 84 | win.wp.hooks.addFilter( 85 | 'blocks.codeBlockPro.themes', 86 | 'testing-testing', 87 | () => ({ nord: { name: 'Nord', priority: true } }), 88 | ); 89 | cy.openSideBarPanel('Theme'); 90 | cy.get(`[href^="${MORE_THEMES_URL}"]`).should('not.exist'); 91 | cy.get('[data-cy="manage-themes"]').should('exist').click(); 92 | cy.get(`[href^="${MORE_THEMES_URL}"]`).should('not.exist'); 93 | cy.get('.code-block-pro-editor [aria-label^="Close"]').click(); 94 | }); 95 | }); 96 | 97 | it('Themes render properly', () => { 98 | Object.keys(themes) 99 | .sort(() => Math.random() - 0.5) 100 | .slice(0, 1) 101 | .forEach((theme) => { 102 | cy.setTheme(theme); 103 | cy.get(`#code-block-pro-theme-${theme}`) 104 | .should('exist') 105 | .should('not.have.text', 'Loading...') 106 | .should('not.have.text', 'ssembly'); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | import { addCode } from './features/code'; 2 | import { setFooter } from './features/footers'; 3 | import { setHeader } from './features/headers'; 4 | import { setHeightDesign, enableMaxHeight } from './features/height'; 5 | import { setLanguage } from './features/language'; 6 | import { setTheme } from './features/theme'; 7 | import { 8 | addBlock, 9 | openBlockInserter, 10 | closeBlockInserter, 11 | openBlockSettingsSideBar, 12 | openSideBarPanel, 13 | saveDraft, 14 | setPostContent, 15 | wpDataSelect, 16 | previewCurrentPage, 17 | focusBlock, 18 | findBlock, 19 | } from './gutenberg'; 20 | import { login, logout } from './login-logout'; 21 | import { 22 | visitPageEditor, 23 | visitPostEditor, 24 | visitAdminPage, 25 | visitToLoginPage, 26 | } from './navigate-pages'; 27 | import { installPlugin, uninstallPlugin } from './plugins'; 28 | import { resetDatabase, switchWpLanguage } from './wp-cli'; 29 | 30 | // Port more commands from WP here: 31 | // https://github.com/WordPress/gutenberg/tree/trunk/packages/e2e-test-utils/src 32 | 33 | // Getting around 34 | Cypress.Commands.add('visitLoginPage', (query) => visitToLoginPage(query)); 35 | Cypress.Commands.add('visitAdminPage', (path, query) => 36 | visitAdminPage(path, query), 37 | ); 38 | Cypress.Commands.add('visitNewPageEditor', (query) => visitPageEditor(query)); 39 | Cypress.Commands.add('visitNewPostEditor', (query) => visitPostEditor(query)); 40 | 41 | // Login logout 42 | Cypress.Commands.add('loginUser', (username, password) => 43 | login(username, password), 44 | ); 45 | Cypress.Commands.add('logoutUser', () => logout()); 46 | 47 | // Gutenberg 48 | Cypress.Commands.add('saveDraft', () => saveDraft()); 49 | Cypress.Commands.add('openBlockInserter', () => openBlockInserter()); 50 | Cypress.Commands.add('closeBlockInserter', () => closeBlockInserter()); 51 | Cypress.Commands.add('openBlockSettingsSideBar', () => 52 | openBlockSettingsSideBar(), 53 | ); 54 | Cypress.Commands.add('openSideBarPanel', (label) => openSideBarPanel(label)); 55 | Cypress.Commands.add('addBlock', (slug) => addBlock(slug)); 56 | Cypress.Commands.add('setPostContent', (content) => setPostContent(content)); 57 | 58 | Cypress.Commands.add('findBlock', findBlock); 59 | Cypress.Commands.add('focusBlock', focusBlock); 60 | Cypress.Commands.add('getCurrentPostObject', () => { 61 | cy.wpDataSelect('core/editor', 'getCurrentPost'); 62 | }); 63 | Cypress.Commands.add('wpDataSelect', (store, selector, ...parameters) => 64 | wpDataSelect(store, selector, ...parameters), 65 | ); 66 | Cypress.Commands.add('previewCurrentPage', () => previewCurrentPage()); 67 | 68 | // Server 69 | Cypress.Commands.add('resetDatabase', resetDatabase); 70 | Cypress.Commands.add('switchWpLang', switchWpLanguage); 71 | // Manage plugins 72 | Cypress.Commands.add('installPlugin', (slug) => installPlugin(slug)); 73 | Cypress.Commands.add('uninstallPlugin', (slug) => uninstallPlugin(slug)); 74 | 75 | // Features 76 | Cypress.Commands.add('setLanguage', (language) => setLanguage(language)); 77 | Cypress.Commands.add('setTheme', (theme) => setTheme(theme)); 78 | Cypress.Commands.add('addCode', (code, opts) => addCode(code, opts)); 79 | Cypress.Commands.add('setHeader', (header) => setHeader(header)); 80 | Cypress.Commands.add('setFooter', (footer) => setFooter(footer)); 81 | Cypress.Commands.add('setHeightDesign', (footer) => setHeightDesign(footer)); 82 | Cypress.Commands.add('enableMaxHeight', (footer) => enableMaxHeight(footer)); 83 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | import 'cypress-real-events'; 2 | import './commands'; 3 | import './helpers'; 4 | -------------------------------------------------------------------------------- /cypress/support/features/code.js: -------------------------------------------------------------------------------- 1 | export const addCode = (code, opts) => { 2 | cy.focusBlock( 3 | 'code-block-pro', 4 | 'textarea.npm__react-simple-code-editor__textarea', 5 | ); 6 | cy.findBlock( 7 | 'code-block-pro', 8 | 'textarea.npm__react-simple-code-editor__textarea', 9 | ) 10 | .should('have.focus') 11 | .type(code, opts); 12 | cy.findBlock( 13 | 'code-block-pro', 14 | 'textarea.npm__react-simple-code-editor__textarea', 15 | ).clear(); 16 | cy.findBlock( 17 | 'code-block-pro', 18 | 'textarea.npm__react-simple-code-editor__textarea', 19 | ).type(code, opts); 20 | cy.findBlock( 21 | 'code-block-pro', 22 | 'textarea.npm__react-simple-code-editor__textarea', 23 | ).should('have.value', opts?.codeOutput ?? code); 24 | }; 25 | -------------------------------------------------------------------------------- /cypress/support/features/footers.js: -------------------------------------------------------------------------------- 1 | export const setFooter = (footer) => { 2 | cy.openBlockSettingsSideBar(); 3 | cy.openSideBarPanel('Footer Type'); 4 | cy.get('div[aria-label="Editor settings"] button') 5 | .contains('Footer Type') 6 | .parents('.editor-sidebar') 7 | .scrollTo('bottom'); 8 | cy.get(`#code-block-pro-footer-${footer}`).should('exist'); 9 | cy.get(`#code-block-pro-footer-${footer}`).click(); 10 | }; 11 | -------------------------------------------------------------------------------- /cypress/support/features/headers.js: -------------------------------------------------------------------------------- 1 | export const setHeader = (header) => { 2 | cy.openBlockSettingsSideBar(); 3 | cy.openSideBarPanel('Header Type'); 4 | cy.get('div[aria-label="Editor settings"] button') 5 | .contains('Header Type') 6 | .parents('.editor-sidebar') 7 | .scrollTo('bottom'); 8 | cy.get(`#code-block-pro-header-${header}`).should('exist'); 9 | cy.get(`#code-block-pro-header-${header}`).click(); 10 | }; 11 | -------------------------------------------------------------------------------- /cypress/support/features/height.js: -------------------------------------------------------------------------------- 1 | export const setHeightDesign = (height) => { 2 | cy.openBlockSettingsSideBar(); 3 | cy.openSideBarPanel('Max Height'); 4 | cy.get('div[aria-label="Editor settings"] button') 5 | .contains('Max Height') 6 | .parents('.editor-sidebar') 7 | .scrollTo('bottom'); 8 | cy.get(`#code-block-pro-see-more-${height}`).should('exist'); 9 | cy.get(`#code-block-pro-see-more-${height}`).click(); 10 | }; 11 | 12 | export const enableMaxHeight = () => { 13 | cy.openBlockSettingsSideBar(); 14 | cy.openSideBarPanel('Max Height'); 15 | cy.get('[data-cy="enable-max-height"]').should('exist'); 16 | cy.get('[data-cy="enable-max-height"]').should('not.be.checked'); 17 | cy.get('[data-cy="enable-max-height"]').check(); 18 | cy.get('[data-cy="enable-max-height"]').should('be.checked'); 19 | }; 20 | -------------------------------------------------------------------------------- /cypress/support/features/language.js: -------------------------------------------------------------------------------- 1 | export const setLanguage = (language) => { 2 | cy.openBlockSettingsSideBar(); 3 | cy.openSideBarPanel('Language'); 4 | cy.get('[data-cy-cbp="language-select"]').select(language); 5 | cy.get('[data-cy-cbp="language-select"]') 6 | .invoke('val') 7 | .should('eq', language); 8 | }; 9 | -------------------------------------------------------------------------------- /cypress/support/features/theme.js: -------------------------------------------------------------------------------- 1 | export const setTheme = (theme) => { 2 | cy.openBlockSettingsSideBar(); 3 | cy.openSideBarPanel('Theme'); 4 | cy.get('div[aria-label="Editor settings"] button') 5 | .contains('Theme') 6 | .parents('.editor-sidebar') 7 | .scrollTo('bottom', { 8 | duration: 300, 9 | }); 10 | cy.get(`#code-block-pro-theme-${theme}`).should('exist'); 11 | cy.get('div[aria-label="Editor settings"] button') 12 | .contains('Theme') 13 | .parents('.editor-sidebar') 14 | .scrollTo('top', { 15 | duration: 300, 16 | }); 17 | cy.get(`#code-block-pro-theme-${theme}`).click(); 18 | }; 19 | -------------------------------------------------------------------------------- /cypress/support/gutenberg.js: -------------------------------------------------------------------------------- 1 | export const saveDraft = () => { 2 | cy.get('body').then((body) => { 3 | if (body.find('.editor-post-save-draft').length > 0) { 4 | cy.get('.editor-post-save-draft').click(); 5 | cy.get('.editor-post-saved-state.is-saved'); 6 | } 7 | }); 8 | }; 9 | 10 | export const setPostContent = (content) => { 11 | cy.window().then((win) => { 12 | const { dispatch } = win.wp.data; 13 | const blocks = win.wp.blocks.parse(content); 14 | dispatch('core/block-editor').resetBlocks(blocks); 15 | }); 16 | }; 17 | export const openBlockInserter = () => { 18 | cy.get('button[aria-label="Toggle block inserter"]').then((button) => { 19 | if (button.attr('aria-pressed') === 'false') { 20 | cy.get('button[aria-label="Toggle block inserter"]').click({ 21 | force: true, 22 | }); 23 | } 24 | }); 25 | }; 26 | export const closeBlockInserter = () => { 27 | cy.get('button[aria-label="Toggle block inserter"]').then((button) => { 28 | if (button.attr('aria-pressed') === 'true') { 29 | cy.get('button[aria-label="Toggle block inserter"]').click(); 30 | } 31 | }); 32 | }; 33 | export const openBlockSettingsSideBar = () => { 34 | cy.get('button[aria-label="Settings"]').then((button) => { 35 | if (button.attr('aria-pressed') === 'false') { 36 | button.trigger('click'); 37 | cy.get('button[aria-label="Settings"]').should( 38 | 'have.attr', 39 | 'aria-pressed', 40 | 'true', 41 | ); 42 | } 43 | }); 44 | }; 45 | export const openSideBarPanel = (label) => { 46 | cy.openBlockSettingsSideBar(); 47 | cy.get('div[aria-label="Editor settings"] button') 48 | .contains(label) 49 | .then((button) => { 50 | if (button.attr('aria-expanded') === 'false') { 51 | button.trigger('click'); 52 | cy.get('div[aria-label="Editor settings"] button') 53 | .contains(label) 54 | .should('have.attr', 'aria-expanded', 'true'); 55 | } 56 | }); 57 | }; 58 | export const addBlock = (slug) => { 59 | cy.window().then((win) => { 60 | cy.get('iframe[name="editor-canvas"]') 61 | .should('exist') 62 | .then(() => { 63 | const block = win.wp.blocks.createBlock(slug); 64 | win.wp.data.dispatch('core/block-editor').insertBlock(block); 65 | }); 66 | }); 67 | }; 68 | export const wpDataSelect = (store, selector, ...parameters) => { 69 | cy.window().then((win) => { 70 | return win.wp.data.select(store)[selector](...parameters); 71 | }); 72 | }; 73 | 74 | export const previewCurrentPage = () => { 75 | cy.saveDraft(); 76 | cy.url().then((url) => { 77 | const page = url.split('post=')[1].split('&')[0]; 78 | cy.visit(`/?page_id=${page}&preview=true`); 79 | }); 80 | cy.get('body').should('not.be.empty'); 81 | }; 82 | export const findBlock = (block, addon = '') => 83 | // eslint-disable-next-line cypress/no-unnecessary-waiting 84 | cy 85 | .get('iframe[name="editor-canvas"]') 86 | .should('exist') 87 | .wait(500) 88 | .then((body) => 89 | cy 90 | .wrap(body.contents()) 91 | .find(`.wp-block[data-type*="${block}"] ${addon}`), 92 | ); 93 | 94 | export const focusBlock = (blockName, addon = '') => { 95 | findBlock(blockName, addon).click(); 96 | }; 97 | -------------------------------------------------------------------------------- /cypress/support/helpers.js: -------------------------------------------------------------------------------- 1 | // This will let you set the text of an imput 2 | // with get('.foo').text('bar') 3 | Cypress.Commands.add('text', { prevSubject: true }, (subject, text) => { 4 | subject.val(text); 5 | return cy.wrap(subject); 6 | }); 7 | Cypress.Commands.add('clearBrowserStorage', () => { 8 | cy.log('Clear browser local storage (including session storage)'); 9 | cy.window().then((win) => { 10 | win.localStorage.clear(); 11 | win.sessionStorage.clear(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /cypress/support/login-logout.js: -------------------------------------------------------------------------------- 1 | export const login = (username = 'admin', password = 'password') => { 2 | cy.log('Login with username: ' + username + ' and password: ' + password); 3 | cy.visitLoginPage() 4 | .get('#user_login') 5 | .text(username) 6 | .get('#user_pass') 7 | .text(password) 8 | .get('#wp-submit') 9 | .click(); 10 | }; 11 | 12 | export const logout = () => { 13 | cy.visitAdminPage(); 14 | cy.get('#wp-admin-bar-logout a').click({ force: true }); 15 | }; 16 | -------------------------------------------------------------------------------- /cypress/support/navigate-pages.js: -------------------------------------------------------------------------------- 1 | import { addQueryArgs } from '@wordpress/url'; 2 | 3 | export const visitToLoginPage = (query = '') => { 4 | const question = query.startsWith('?') ? '' : '?'; 5 | cy.visit(`wp-login.php${question}${query}`); 6 | }; 7 | 8 | export const visitAdminPage = (adminPath = '', query = '') => { 9 | const question = query.startsWith('?') ? '' : '?'; 10 | cy.visit(`wp-admin/${adminPath}${question}${query}`); 11 | }; 12 | 13 | const visitEditor = (query) => { 14 | query = addQueryArgs('', { 15 | post_type: 'page', 16 | ...query, 17 | }).slice(1); 18 | 19 | cy.visitAdminPage('post-new.php', query); 20 | }; 21 | export const visitPageEditor = (query) => 22 | visitEditor({ ...query, post_type: 'page' }); 23 | export const visitPostEditor = (query) => 24 | visitEditor({ ...query, post_type: 'post' }); 25 | -------------------------------------------------------------------------------- /cypress/support/plugins.js: -------------------------------------------------------------------------------- 1 | export const installPlugin = (slug) => { 2 | cy.visitAdminPage( 3 | 'plugin-install.php', 4 | 's=' + encodeURIComponent(slug) + '&tab=search&type=term', 5 | ); 6 | cy.get(`.plugin-card-${slug}`).then((card) => { 7 | const activeButton = 8 | ''; 9 | if (card.html().includes(activeButton)) return; 10 | if (card.html().includes('Install Now')) { 11 | cy.get( 12 | `.plugin-card-${slug} a[href*="${slug}"].install-now`, 13 | ).click(); 14 | } 15 | cy.get(`.plugin-card-${slug} a[href*="${slug}"].activate-now`).click(); 16 | }); 17 | }; 18 | 19 | export const uninstallPlugin = (slug) => { 20 | cy.visitAdminPage('plugins.php'); 21 | cy.get(`tr[data-slug=${slug}]`).then((plugin) => { 22 | // If active, deactivate first 23 | if (plugin.html().includes(`deactivate-${slug}`)) { 24 | cy.get(`#deactivate-${slug}`).click({ force: true }); 25 | } 26 | cy.get(`#delete-${slug}`).click({ force: true }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /cypress/support/wp-cli.js: -------------------------------------------------------------------------------- 1 | export const resetDatabase = () => { 2 | cy.exec('wp-env clean all'); 3 | cy.exec( 4 | 'wp-env run cli wp user meta add 1 wp_persisted_preferences \'{"core/edit-post":{"welcomeGuide":false,"core/edit-post/pattern-modal":false,"pattern-modal":false,"edit-post/pattern-modal":false,"patternModal":false},"core":{"enableChoosePatternModal":false},"_modified":"2025-03-23T02:16:33.561Z"}\' --format=json', 5 | ); 6 | }; 7 | 8 | export const switchWpLanguage = (language) => { 9 | // install first 10 | cy.exec(`wp-env run cli wp language core install ${language}`); 11 | cy.exec(`wp-env run cli wp site switch-language ${language}`); 12 | }; 13 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules } from '@eslint/compat'; 2 | import { FlatCompat } from '@eslint/eslintrc'; 3 | import js from '@eslint/js'; 4 | import tsParser from '@typescript-eslint/parser'; 5 | import noOnlyTests from 'eslint-plugin-no-only-tests'; 6 | import path from 'node:path'; 7 | import { fileURLToPath } from 'node:url'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | export default [ 18 | { 19 | ignores: ['**/node_modules/', '**/build/', 'postcss.config.js'], 20 | }, 21 | ...fixupConfigRules( 22 | compat.extends( 23 | 'eslint:recommended', 24 | 'plugin:@typescript-eslint/eslint-recommended', 25 | 'plugin:@typescript-eslint/recommended', 26 | 'plugin:react/recommended', 27 | 'plugin:react-hooks/recommended', 28 | 'plugin:jsx-a11y/recommended', 29 | 'plugin:cypress/recommended', 30 | 'prettier', 31 | ), 32 | ), 33 | { 34 | plugins: { 35 | 'no-only-tests': noOnlyTests, 36 | }, 37 | 38 | languageOptions: { 39 | parser: tsParser, 40 | ecmaVersion: 5, 41 | sourceType: 'module', 42 | 43 | parserOptions: { 44 | ecmaFeatures: { 45 | jsx: true, 46 | }, 47 | 48 | allowImportExportEverywhere: true, 49 | }, 50 | }, 51 | 52 | settings: { 53 | react: { 54 | version: 'detect', 55 | }, 56 | }, 57 | 58 | rules: { 59 | 'require-await': 'error', 60 | 61 | quotes: [ 62 | 'error', 63 | 'single', 64 | { 65 | avoidEscape: true, 66 | }, 67 | ], 68 | 69 | 'comma-dangle': ['error', 'always-multiline'], 70 | 'array-element-newline': ['error', 'consistent'], 71 | 72 | 'no-constant-condition': [ 73 | 'error', 74 | { 75 | checkLoops: true, 76 | }, 77 | ], 78 | 79 | 'no-multi-spaces': ['error'], 80 | semi: ['error', 'always'], 81 | 'space-in-parens': ['error', 'never'], 82 | 83 | 'key-spacing': [ 84 | 'error', 85 | { 86 | afterColon: true, 87 | }, 88 | ], 89 | 90 | 'no-only-tests/no-only-tests': 'warn', 91 | 'space-infix-ops': ['error'], 92 | 93 | 'space-before-function-paren': [ 94 | 'error', 95 | { 96 | anonymous: 'always', 97 | named: 'never', 98 | asyncArrow: 'always', 99 | }, 100 | ], 101 | 102 | 'react/react-in-jsx-scope': 'off', 103 | 'quote-props': ['error', 'as-needed'], 104 | 105 | 'no-multiple-empty-lines': [ 106 | 'error', 107 | { 108 | max: 1, 109 | }, 110 | ], 111 | 112 | '@typescript-eslint/no-var-requires': 'off', 113 | 114 | 'lines-around-comment': [ 115 | 'error', 116 | { 117 | beforeBlockComment: true, 118 | allowBlockStart: true, 119 | }, 120 | ], 121 | }, 122 | }, 123 | ]; 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-block-pro", 3 | "version": "0.1.0", 4 | "description": "Code Block Pro - Beautiful syntax highlighting", 5 | "author": "Kevin Batdorf", 6 | "license": "GPL-2.0-or-later", 7 | "main": "build/index.js", 8 | "scripts": { 9 | "build": "wp-scripts build", 10 | "format": "wp-scripts format", 11 | "lint:css": "wp-scripts lint-style", 12 | "lint:js": "wp-scripts lint-js", 13 | "packages-update": "wp-scripts packages-update", 14 | "plugin-zip": "wp-scripts plugin-zip", 15 | "start": "wp-scripts start", 16 | "start:hot": "wp-scripts start --hot", 17 | "eject": "node scripts/eject.mjs", 18 | "rename": "node scripts/rename.mjs" 19 | }, 20 | "devDependencies": { 21 | "@redux-devtools/extension": "^3.3.0", 22 | "@trivago/prettier-plugin-sort-imports": "5.2.2", 23 | "@types/wordpress__block-editor": "11.5.16", 24 | "@types/wordpress__blocks": "12.5.17", 25 | "@types/wordpress__edit-post": "^8.4.2", 26 | "@typescript-eslint/eslint-plugin": "^8.32.0", 27 | "@typescript-eslint/parser": "8.32.0", 28 | "@wordpress/block-editor": "14.18.0", 29 | "@wordpress/env": "10.23.0", 30 | "@wordpress/scripts": "30.16.0", 31 | "@wordpress/url": "4.23.0", 32 | "autoprefixer": "10.4.21", 33 | "copy-webpack-plugin": "13.0.0", 34 | "cypress": "14.3.3", 35 | "cypress-real-events": "^1.14.0", 36 | "eslint": "9.26.0", 37 | "eslint-config-prettier": "10.1.5", 38 | "eslint-plugin-cypress": "4.3.0", 39 | "eslint-plugin-no-only-tests": "^3.3.0", 40 | "eslint-plugin-prettier": "5.4.0", 41 | "eslint-plugin-react": "7.37.5", 42 | "eslint-plugin-react-hooks": "rc", 43 | "fast-glob": "3.3.3", 44 | "glob": "11.0.2", 45 | "postcss-import": "16.1.0", 46 | "postcss-safe-important": "2.0.1", 47 | "prettier": "3.5.3", 48 | "replace-in-file": "8.3.0", 49 | "rtlcss-webpack-plugin": "^4.0.7", 50 | "tailwindcss": "3.4.17", 51 | "typescript": "5.8.3", 52 | "webpack": "^5.99.8" 53 | }, 54 | "dependencies": { 55 | "@eslint/compat": "^1.2.9", 56 | "@headlessui/react": "2.2.2", 57 | "@wordpress/block-editor": "14.18.0", 58 | "@wordpress/blocks": "14.12.0", 59 | "@wordpress/edit-post": "^8.23.0", 60 | "@wordpress/element": "6.23.0", 61 | "@wordpress/escape-html": "^3.23.0", 62 | "@wordpress/hooks": "4.23.0", 63 | "@wordpress/html-entities": "^4.23.0", 64 | "@wordpress/i18n": "5.23.0", 65 | "classnames": "^2.5.1", 66 | "copy-to-clipboard": "3.3.3", 67 | "react-simple-code-editor": "0.14.1", 68 | "shiki": "0.14.7", 69 | "strip-ansi": "^7.1.0", 70 | "swr": "2.3.3", 71 | "zustand": "5.0.4" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /php/compatibility.php: -------------------------------------------------------------------------------- 1 | tags in the content, resulting in escaped html. 8 | * https://plugins.trac.wordpress.org/browser/prismatic/trunk/inc/prismatic-core.php#L59 9 | * Will monitor that plugin periodically and remove this override if it is updated. 10 | */ 11 | if (!class_exists('Prismatic')) { 12 | class Prismatic 13 | { 14 | public static function options_general() 15 | { 16 | } 17 | public static function options_advanced() 18 | { 19 | } 20 | public static function options_prism() 21 | { 22 | } 23 | public static function options_highlight() 24 | { 25 | } 26 | public static function options_plain() 27 | { 28 | } 29 | } 30 | function prismatic() 31 | { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /php/router.php: -------------------------------------------------------------------------------- 1 | 'GET', 17 | 'callback' => $callback, 18 | 'permission_callback' => function () { 19 | return \current_user_can($this->capability); 20 | }, 21 | ] 22 | ); 23 | } 24 | public function postHandler($namespace, $endpoint, $callback) 25 | { 26 | \register_rest_route( 27 | $namespace, 28 | $endpoint, 29 | [ 30 | 'methods' => 'POST', 31 | 'callback' => $callback, 32 | 'permission_callback' => function () { 33 | return \current_user_can($this->capability); 34 | }, 35 | ] 36 | ); 37 | } 38 | 39 | public static function __callStatic($name, array $arguments) 40 | { 41 | $name = "{$name}Handler"; 42 | if (is_null(self::$instance)) { 43 | self::$instance = new static(); 44 | } 45 | $r = self::$instance; 46 | return $r->$name($r->namespace, ...$arguments); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /php/routes.php: -------------------------------------------------------------------------------- 1 | get_option('code_block_pro_settings'), 16 | 'code_block_pro_settings_2' => get_option('code_block_pro_settings_2'), 17 | ] 18 | ); 19 | }); 20 | 21 | CBPRouter::get('/settings', function () { 22 | return new WP_REST_Response( 23 | [ 24 | 'code_block_pro_settings' => get_option('code_block_pro_settings'), 25 | 'code_block_pro_settings_2' => get_option('code_block_pro_settings_2'), 26 | ] 27 | ); 28 | }); 29 | 30 | CBPRouter::get('/can-save-html', function () { 31 | return new WP_REST_Response(current_user_can('unfiltered_html')); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const tailwind = require('./tailwind.config'); 2 | 3 | module.exports = ({ mode, file }) => ({ 4 | ident: 'postcss', 5 | sourceMap: mode !== 'production', 6 | plugins: [ 7 | require('postcss-import'), 8 | require('tailwindcss/nesting'), 9 | require('tailwindcss')({ 10 | ...tailwind, 11 | // Scope the editor css separately from the frontend css. 12 | content: file.endsWith('editor.css') 13 | ? ['./src/editor/**/*.{ts,tsx}'] 14 | : ['./src/front/**/*.{ts,tsx}'], 15 | important: 16 | tailwind.important + 17 | (file.endsWith('editor.css') ? '-editor' : ''), 18 | }), 19 | (css) => 20 | css.walkRules((rule) => { 21 | // Removes top level TW styles like *::before {} 22 | rule.selector.startsWith('*') && rule.remove(); 23 | }), 24 | // See: https://github.com/WordPress/gutenberg/blob/trunk/packages/postcss-plugins-preset/lib/index.js 25 | require('autoprefixer')({ grid: true }), 26 | !file.endsWith('editor.css') && require('postcss-safe-important'), 27 | mode === 'production' && 28 | // See: https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/config/webpack.config.js#L68 29 | require('cssnano')({ 30 | preset: [ 31 | 'default', 32 | { 33 | discardComments: { 34 | removeAll: true, 35 | }, 36 | }, 37 | ], 38 | }), 39 | ], 40 | }); 41 | -------------------------------------------------------------------------------- /src/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "kevinbatdorf/code-block-pro", 5 | "version": "1.13.0", 6 | "title": "Code Block Pro - Beautiful syntax highlighting", 7 | "category": "common", 8 | "description": "Code highlighting powered by the VS Code engine. No overhead.", 9 | "attributes": { 10 | "code": { "type": "string" }, 11 | "codeHTML": { "type": "string" }, 12 | "language": { "type": "string" }, 13 | "theme": { "type": "string" }, 14 | "align": { "type": "string" }, 15 | "bgColor": { "type": "string", "default": "#282a37" }, 16 | "textColor": { "type": "string", "default": "#f8f8f2" }, 17 | "fontSize": { "type": "string" }, 18 | "fontFamily": { "type": "string" }, 19 | "lineHeight": { "type": "string" }, 20 | "lineNumbers": { "type": "boolean" }, 21 | "clampFonts": { "type": "boolean" }, 22 | "editorHeight": { "type": "string" }, 23 | "headerType": { "type": "string" }, 24 | "footerType": { "type": "string" }, 25 | "headerString": { "type": "string" }, 26 | "disablePadding": { "type": "boolean" }, 27 | "footerString": { "type": "string" }, 28 | "footerLink": { "type": "string" }, 29 | "enableMaxHeight": { "type": "boolean" }, 30 | "seeMoreType": { "type": "string" }, 31 | "seeMoreString": { "type": "string" }, 32 | "seeMoreAfterLine": { "type": "string" }, 33 | "seeMoreTransition": { "type": "boolean" }, 34 | "seeMoreCollapse": { "type": "boolean" }, 35 | "seeMoreCollapseString": { "type": "string" }, 36 | "footerLinkTarget": { "type": "boolean" }, 37 | "startingLineNumber": { "type": "number", "default": 1 }, 38 | "lineNumbersWidth": { "type": "number" }, 39 | "enableHighlighting": { "type": "boolean" }, 40 | "highlightingHover": { "type": "boolean" }, 41 | "lineHighlights": { "type": "string" }, 42 | "lineHighlightColor": { "type": "string" }, 43 | "enableBlurring": { "type": "boolean" }, 44 | "lineBlurs": { "type": "string" }, 45 | "removeBlurOnHover": { "type": "boolean" }, 46 | "frame": { "type": "boolean" }, 47 | "renderType": { "type": "string", "default": "code" }, 48 | "label": { "type": "string", "default": "" }, 49 | "copyButton": { "type": "boolean" }, 50 | "copyButtonType": { "type": "string" }, 51 | "copyButtonString": { "type": "string" }, 52 | "copyButtonStringCopied": { "type": "string" }, 53 | "copyButtonUseTextarea": { "type": "boolean", "default": false }, 54 | "tabSize": { "type": "number" }, 55 | "useTabs": { "type": "boolean" } 56 | }, 57 | "supports": { 58 | "html": false, 59 | "align": ["wide", "full"] 60 | }, 61 | "textdomain": "code-block-pro", 62 | "editorScript": "file:./index.js", 63 | "editorStyle": "file:./index.css", 64 | "style": "file:./style-index.css", 65 | "viewScript": "file:./front/front.js" 66 | } 67 | -------------------------------------------------------------------------------- /src/defaultThemes.json: -------------------------------------------------------------------------------- 1 | { 2 | "dark-plus": { "name": "Dark Plus" }, 3 | "dracula-soft": { "name": "Dracula Soft" }, 4 | "dracula": { "name": "Dracula" }, 5 | "github-dark-dimmed": { "name": "GitHub Dark Dimmed" }, 6 | "github-dark": { "name": "Github Dark" }, 7 | "github-light": { "name": "Github Light" }, 8 | "light-plus": { "name": "Light Plus" }, 9 | "material-darker": { 10 | "name": "Material Darker", 11 | "alias": "material-theme-darker" 12 | }, 13 | "material-default": { 14 | "name": "Material Default", 15 | "alias": "material-theme" 16 | }, 17 | "material-lighter": { 18 | "name": "Material Lighter", 19 | "alias": "material-theme-lighter" 20 | }, 21 | "material-ocean": { 22 | "name": "Material Ocean", 23 | "alias": "material-theme-ocean" 24 | }, 25 | "material-palenight": { 26 | "name": "Material Palenight", 27 | "alias": "material-theme-palenight" 28 | }, 29 | "min-dark": { "name": "Min Dark" }, 30 | "min-light": { "name": "Min Light" }, 31 | "monokai": { "name": "Monokai" }, 32 | "nord": { "name": "Nord" }, 33 | "one-dark-pro": { "name": "One Dark Pro" }, 34 | "poimandres": { "name": "Poimandres" }, 35 | "rose-pine-dawn": { "name": "Rose Pine Dawn" }, 36 | "rose-pine-moon": { "name": "Rose Pine Moon" }, 37 | "rose-pine": { "name": "Rose Pine" }, 38 | "slack-dark": { "name": "Slack Dark" }, 39 | "slack-ochin": { "name": "Slack Ochin" }, 40 | "solarized-dark": { "name": "Solarized Dark" }, 41 | "solarized-light": { "name": "Solarized Light" }, 42 | "vitesse-dark": { "name": "Vitesse Dark" }, 43 | "vitesse-light": { "name": "Vitesse Light" } 44 | } 45 | -------------------------------------------------------------------------------- /src/editor/components/BlockFilter.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | store as blockEditorStore, 3 | BlockControls, 4 | } from '@wordpress/block-editor'; 5 | import { createBlock } from '@wordpress/blocks'; 6 | import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; 7 | import { useSelect, useDispatch } from '@wordpress/data'; 8 | import { __ } from '@wordpress/i18n'; 9 | import blockConfig from '../../block.json'; 10 | import { blockIcon } from '../../icons'; 11 | import { useLanguageStore } from '../../state/language'; 12 | import { getMainAlias } from '../../util/languages'; 13 | 14 | export const BlockFilter = ( 15 | // eslint-disable-next-line 16 | CurrentMenuItems: any, 17 | // eslint-disable-next-line 18 | props: any, 19 | ) => { 20 | // eslint-disable-next-line 21 | const { attributes, clientId } = props; 22 | const { previousLanguage } = useLanguageStore(); 23 | const showMenu = useSelect( 24 | (select) => { 25 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 26 | // @ts-ignore-next-line - getBlock not added as a type? 27 | const currentBlock = select(blockEditorStore).getBlock(clientId); 28 | return ['core/code', 'syntaxhighlighter/code'].includes( 29 | currentBlock.name, 30 | ); 31 | }, 32 | [clientId], 33 | ); 34 | 35 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 36 | // @ts-ignore-next-line - replaceBlock not added as a type? 37 | const { replaceBlock } = useDispatch(blockEditorStore); 38 | 39 | const decode = (value: string) => { 40 | const txt = document.createElement('textarea'); 41 | txt.innerHTML = value; 42 | return txt.value; 43 | }; 44 | 45 | const convertBlock = () => { 46 | const blockData = createBlock(blockConfig.name, { 47 | // eslint-disable-next-line 48 | code: attributes?.content ? decode(attributes.content) : undefined, 49 | // eslint-disable-next-line 50 | language: getMainAlias(attributes?.language) ?? previousLanguage, 51 | }); 52 | replaceBlock(clientId, [blockData]); 53 | }; 54 | 55 | if (!showMenu) { 56 | return ; 57 | } 58 | 59 | return ( 60 | <> 61 | {CurrentMenuItems && } 62 | 63 | 64 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/editor/components/ButtonsPanel.tsx: -------------------------------------------------------------------------------- 1 | import { PanelBody } from '@wordpress/components'; 2 | import { __ } from '@wordpress/i18n'; 3 | import { AttributesPropsAndSetter } from '../../types'; 4 | import { CopyBtnSettings } from './buttons/sidebar/CopyBtnSettings'; 5 | 6 | export const ButtonsPanel = ({ 7 | attributes, 8 | setAttributes, 9 | }: AttributesPropsAndSetter & { bringAttentionToThemes?: boolean }) => { 10 | return ( 11 |
12 | 15 | 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/editor/components/FooterSelect.tsx: -------------------------------------------------------------------------------- 1 | import { BaseControl } from '@wordpress/components'; 2 | import { sprintf, __ } from '@wordpress/i18n'; 3 | import { Attributes } from '../../types'; 4 | import { SimpleStringEnd } from './footers/SimpleStringEnd'; 5 | import { SimpleStringStart } from './footers/SimpleStringStart'; 6 | 7 | type FooterSelectProps = { 8 | attributes: Attributes; 9 | onClick: (slug: string) => void; 10 | }; 11 | export const FooterSelect = ({ attributes, onClick }: FooterSelectProps) => { 12 | const { footerType, ...attributesWithoutFooterType } = attributes; 13 | const { bgColor } = attributes; 14 | const types = { 15 | none: __('None', 'code-block-pro'), 16 | simpleStringEnd: __('Simple string end', 'code-block-pro'), 17 | simpleStringStart: __('Simple string start', 'code-block-pro'), 18 | }; 19 | 20 | return ( 21 |
22 | {Object.entries(types).map(([slug, type]) => ( 23 | 43 | 59 | 60 | ))} 61 |
62 | ); 63 | }; 64 | 65 | export const FooterType = (props: Attributes) => { 66 | const { footerType } = props; 67 | if (footerType === 'simpleStringEnd') { 68 | return ; 69 | } 70 | if (footerType === 'simpleStringStart') { 71 | return ; 72 | } 73 | 74 | return null; 75 | }; 76 | -------------------------------------------------------------------------------- /src/editor/components/HeaderSelect.tsx: -------------------------------------------------------------------------------- 1 | import { BaseControl } from '@wordpress/components'; 2 | import { sprintf, __ } from '@wordpress/i18n'; 3 | import { Attributes } from '../../types'; 4 | import { Headlights } from './headers/Headlights'; 5 | import { HeadlightsMuted } from './headers/HeadlightsMuted'; 6 | import { HeadlightsMutedAlt } from './headers/HeadlightsMutedAlt'; 7 | import { PillString } from './headers/PillString'; 8 | import { SimpleString } from './headers/SimpleString'; 9 | import { StringSmall } from './headers/StringSmall'; 10 | import { UnsupportedTheme } from './misc/Unsupported'; 11 | 12 | type HeaderSelectProps = { 13 | attributes: Attributes; 14 | onClick: (slug: string) => void; 15 | }; 16 | 17 | const unsupportedWithCssVars = ['headlightsMutedAlt']; 18 | 19 | export const HeaderSelect = ({ attributes, onClick }: HeaderSelectProps) => { 20 | const { headerType, ...attributesWithoutHeaderType }: Attributes = 21 | attributes; 22 | const { bgColor } = attributes; 23 | const types = { 24 | none: __('None', 'code-block-pro'), 25 | headlights: __('Headlights', 'code-block-pro'), 26 | headlightsMuted: __('Headlights muted', 'code-block-pro'), 27 | headlightsMutedAlt: __('Headlights muted alt', 'code-block-pro'), 28 | simpleString: __('Simple string', 'code-block-pro'), 29 | stringSmall: __('String muted', 'code-block-pro'), 30 | pillString: __('Pill string', 'code-block-pro'), 31 | }; 32 | const isUnsupported = bgColor?.startsWith('var('); 33 | 34 | return ( 35 |
36 | {Object.entries(types).map(([slug, type]) => ( 37 | 59 | 75 | {isUnsupported && unsupportedWithCssVars.includes(slug) && ( 76 | 77 | )} 78 | 79 | ))} 80 |
81 | ); 82 | }; 83 | 84 | export const HeaderType = (attributes: Attributes) => { 85 | const { headerType, bgColor } = attributes; 86 | const isACustomTheme = bgColor?.startsWith('var('); 87 | if (isACustomTheme && unsupportedWithCssVars.includes(headerType ?? '')) { 88 | return null; 89 | } 90 | 91 | if (headerType === 'headlights') { 92 | return ; 93 | } 94 | if (headerType === 'headlightsMuted') { 95 | return ; 96 | } 97 | if (headerType === 'headlightsMutedAlt') { 98 | return ; 99 | } 100 | if (headerType === 'simpleString') { 101 | return ; 102 | } 103 | if (headerType === 'stringSmall') { 104 | return ; 105 | } 106 | if (headerType === 'pillString') { 107 | return ; 108 | } 109 | return null; 110 | }; 111 | -------------------------------------------------------------------------------- /src/editor/components/SeeMoreSelect.tsx: -------------------------------------------------------------------------------- 1 | import { BaseControl } from '@wordpress/components'; 2 | import { sprintf, __ } from '@wordpress/i18n'; 3 | import { Attributes } from '../../types'; 4 | import { BlockLeft } from './seemore/BlockLeft'; 5 | import { BlockRight } from './seemore/BlockRight'; 6 | import { RoundCenter } from './seemore/RoundCenter'; 7 | 8 | type SeeMoreSelectProps = { 9 | attributes: Attributes; 10 | onClick: (slug: string) => void; 11 | }; 12 | export const SeeMoreSelect = ({ attributes, onClick }: SeeMoreSelectProps) => { 13 | const { 14 | seeMoreType, 15 | ...attributesWithoutSeeMoreType 16 | }: Partial = attributes; 17 | const { bgColor } = attributes; 18 | const types = { 19 | none: __('None', 'code-block-pro'), 20 | roundCenter: __('See more center', 'code-block-pro'), 21 | blockLeft: __('See more left', 'code-block-pro'), 22 | blockRight: __('See more right', 'code-block-pro'), 23 | }; 24 | 25 | return ( 26 |
27 | {Object.entries(types).map(([slug, type]) => ( 28 | 40 | 57 | 58 | ))} 59 |
60 | ); 61 | }; 62 | 63 | export const SeeMoreType = ( 64 | props: Partial & { context?: string }, 65 | ) => { 66 | const { seeMoreType, enableMaxHeight, context } = props; 67 | if (!enableMaxHeight) return null; 68 | if (seeMoreType === 'roundCenter') { 69 | return ; 70 | } 71 | if (seeMoreType === 'blockLeft') { 72 | return ; 73 | } 74 | if (seeMoreType === 'blockRight') { 75 | return ; 76 | } 77 | 78 | return null; 79 | }; 80 | -------------------------------------------------------------------------------- /src/editor/components/SlotFactory.tsx: -------------------------------------------------------------------------------- 1 | import { createSlotFill } from '@wordpress/components'; 2 | import { useMemo } from '@wordpress/element'; 3 | import { AttributesPropsAndSetter } from '../../types'; 4 | import { decode, encode } from '../../util/code'; 5 | import { languages } from '../../util/languages'; 6 | 7 | export const SlotFactory = ({ 8 | name, 9 | attributes, 10 | setAttributes, 11 | }: AttributesPropsAndSetter & { name: string }) => { 12 | const { Slot } = useMemo(() => { 13 | return createSlotFill(name); 14 | }, [name]); 15 | return ( 16 | decode(attributes.code, attributes), 22 | setCode: (code: string) => 23 | setAttributes({ 24 | code: encode(code, attributes), 25 | }), 26 | }} 27 | /> 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/editor/components/ThemePreview.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from '@wordpress/element'; 2 | import { decodeEntities } from '@wordpress/html-entities'; 3 | import { __ } from '@wordpress/i18n'; 4 | import { Theme } from 'shiki'; 5 | import { useTheme } from '../../hooks/useTheme'; 6 | import { CustomStyles, Lang } from '../../types'; 7 | import { fontFamilyLong, maybeClamp } from '../../util/fonts'; 8 | 9 | type ThemePreviewProps = { 10 | id: string; 11 | theme: Theme; 12 | lang: Lang; 13 | code: string; 14 | fontSize: string; 15 | lineHeight: string; 16 | fontFamily: string; 17 | clampFonts: boolean; 18 | styles?: CustomStyles; 19 | onClick: () => void; 20 | }; 21 | export const ThemePreview = ({ 22 | id, 23 | theme, 24 | lang, 25 | onClick, 26 | code, 27 | fontSize, 28 | lineHeight, 29 | fontFamily, 30 | clampFonts, 31 | styles, 32 | }: ThemePreviewProps) => { 33 | const [inView, setInView] = useState(false); 34 | const { highlighter, error, loading } = useTheme({ 35 | theme, 36 | // If no code is written yet, show a classic Carmack snippet 37 | lang: code ? lang : 'c', 38 | ready: inView, 39 | }); 40 | const [codeRendered, setCode] = useState(''); 41 | const [backgroundColor, setBg] = useState('#ffffff'); 42 | const observer = useRef( 43 | new IntersectionObserver(([entry]) => { 44 | if (entry.isIntersecting) { 45 | setInView(true); 46 | observer.current.disconnect(); 47 | } 48 | }), 49 | ); 50 | const codeSnippet = ` 51 | float Q_rsqrt( float number ) 52 | { 53 | long i; 54 | float x2, y; 55 | const float threehalfs = 1.5F; 56 | 57 | x2 = number * 0.5F; 58 | y = number; 59 | i = * ( long * ) &y; // evil floating point bit level hacking 60 | i = 0x5f3759df - ( i >> 1 ); // what the frack? 61 | y = * ( float * ) &i; 62 | y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration 63 | // y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed 64 | 65 | return y; 66 | }`.trim(); 67 | 68 | useEffect(() => { 69 | if (!highlighter) return; 70 | if ((lang as string) === 'ansi' && code) { 71 | setCode(highlighter.ansiToHtml(code)); 72 | setBg(highlighter.getBackgroundColor()); 73 | return; 74 | } 75 | const hl = code 76 | ? highlighter.codeToHtml(decodeEntities(code), { lang }) 77 | : highlighter.codeToHtml(decodeEntities(codeSnippet), { 78 | lang: 'c', 79 | }); 80 | setCode(hl); 81 | setBg(highlighter.getBackgroundColor()); 82 | }, [highlighter, lang, code, codeSnippet]); 83 | return ( 84 | 123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /src/editor/components/ThemesPanel.tsx: -------------------------------------------------------------------------------- 1 | import { PanelBody } from '@wordpress/components'; 2 | import { useState } from '@wordpress/element'; 3 | import { __ } from '@wordpress/i18n'; 4 | import { Theme } from 'shiki'; 5 | import { useSettingsStoreReady } from '../../state/settings'; 6 | import { useThemeStore } from '../../state/theme'; 7 | import { AttributesPropsAndSetter } from '../../types'; 8 | import { ThemeFilter } from './ThemeFilter'; 9 | import { ThemeSelect } from './ThemeSelect'; 10 | 11 | export const ThemesPanel = ({ 12 | bringAttentionToThemes, 13 | attributes, 14 | setAttributes, 15 | }: AttributesPropsAndSetter & { bringAttentionToThemes?: boolean }) => { 16 | const { updateThemeHistory } = useThemeStore(); 17 | const [search, setSearch] = useState(''); 18 | const ready = useSettingsStoreReady(); 19 | return ( 20 | 23 | {ready && } 24 | {ready && ( 25 | { 29 | setAttributes({ theme }); 30 | updateThemeHistory({ theme }); 31 | }} 32 | /> 33 | )} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/editor/components/buttons/ButtonList.tsx: -------------------------------------------------------------------------------- 1 | import { Attributes } from '../../../types'; 2 | import { CopyButton } from './copy/CopyButton'; 3 | 4 | export const ButtonList = (props: Attributes) => { 5 | const { copyButton } = props; 6 | return copyButton ? : null; 7 | }; 8 | -------------------------------------------------------------------------------- /src/editor/components/buttons/copy/Clipboard.tsx: -------------------------------------------------------------------------------- 1 | export const Clipboard = () => ( 2 | 9 | 15 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /src/editor/components/buttons/copy/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import { __ } from '@wordpress/i18n'; 2 | import stripAnsi from 'strip-ansi'; 3 | import { Attributes } from '../../../../types'; 4 | import { Clipboard } from './Clipboard'; 5 | import { TextSimple } from './TextSimple'; 6 | import { TwoSquares } from './TwoSquares'; 7 | 8 | export const copyButtonTypes = { 9 | // Poorly named key here, but it's already there so it stays 10 | heroicons: { 11 | label: __('Clipboard', 'code-block-pro'), 12 | Component: Clipboard, 13 | }, 14 | twoSquares: { 15 | label: __('Two Squares', 'code-block-pro'), 16 | Component: TwoSquares, 17 | }, 18 | textSimple: { 19 | label: __('Text Simple', 'code-block-pro'), 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | Component: (props: any) => , 22 | }, 23 | }; 24 | 25 | export const CopyButton = ({ attributes }: { attributes: Attributes }) => { 26 | const { 27 | copyButtonType, 28 | copyButtonString, 29 | copyButtonStringCopied, 30 | copyButtonUseTextarea, 31 | useDecodeURI, 32 | code, 33 | textColor, 34 | bgColor: b, 35 | headerType, 36 | } = attributes; 37 | const hasTextButton = copyButtonType === 'textSimple'; 38 | const color = hasTextButton ? b : textColor; 39 | const bgColor = hasTextButton ? textColor : undefined; 40 | const { Component } = 41 | copyButtonTypes?.[copyButtonType as keyof typeof copyButtonTypes] ?? 42 | copyButtonTypes.heroicons; 43 | const codeToCopy = stripAnsi( 44 | useDecodeURI ? encodeURIComponent(code ?? '') : (code ?? ''), 45 | ); 46 | return ( 47 | 69 | {copyButtonUseTextarea ? ( 70 |