diff --git a/.editorconfig b/.editorconfig index 4ca196b1ed12ac..a541e47e767bca 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,14 +1,19 @@ -# http://editorconfig.org +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + root = true [*] -indent_style = tab -end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true +end_of_line = lf insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab -[{package.json,*.yml}] +[*.yml] indent_style = space indent_size = 2 diff --git a/.eslines.json b/.eslines.json deleted file mode 100644 index 46acda6d92df1d..00000000000000 --- a/.eslines.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "branches": { - "default": ["downgrade-unmodified-lines"] - }, - "processors": { - "downgrade-unmodified-lines": { - "remote": "origin/master", - "rulesNotToDowngrade": ["no-unused-vars"] - } - } -} \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 593ead98c86dc4..1322806281c15f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,6 @@ "parser": "babel-eslint", "extends": [ "wordpress", - "plugin:wordpress/jsdoc", "plugin:react/recommended", "plugin:jsx-a11y/recommended", "plugin:jest/recommended" @@ -181,13 +180,5 @@ "valid-jsdoc": [ "error", { "requireReturn": false } ], "valid-typeof": "error", "yoda": "off" - }, - "overrides": [ - { - "files": [ "{blocks,components,date,editor,element,i18n,data,utils}/**/test/*.js" ], - "rules": { - "require-jsdoc": "off" - } - } - ] + } } diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index bdf57282e25538..b0f343355326f5 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,7 +4,7 @@ BEFORE POSTING YOUR ISSUE: - Try to add as much detail as possible. Be specific! - Please add the version of Gutenberg you are using in the description. - If you're requesting a new feature, explain why you'd like it to be added. -- Search this repository for the issue and whether it has been fixed or reported already. +- Search this repository for issues and pull requests and whether it has been fixed or reported already. - Ensure you are using the latest code before logging bugs. - Disable all plugins to ensure it's not a plugin conflict issue. --> diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1e94ad0a912788..9cc8e844328862 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,4 +17,4 @@ ## Checklist: - [ ] My code is tested. - [ ] My code follows the WordPress code style. -- [ ] My code follows has proper inline documentation. \ No newline at end of file +- [ ] My code has proper inline documentation. diff --git a/.gitignore b/.gitignore index 96d44d49cb7fc4..af7f3c37bf9f61 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ gutenberg.zip *.log phpcs.xml yarn.lock +docker-compose.override.yml +cypress.env.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b84a8171aa4b66..5cc184e4382c2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,11 @@ ## Getting Started -Gutenberg is a Node.js-based project, built primarily in JavaScript. Be sure to have Node.js installed first. You should be running a Node version matching the [current active LTS release](https://github.com/nodejs/Release#release-schedule) or newer for this plugin to work correctly. You can check your Node.js version by typing `node -v` in the Terminal prompt. +Gutenberg is a Node.js-based project, built primarily in JavaScript. + +The easiest way to get started is by running the Local Environment setup script, `./bin/setup-local-env.sh`. This will check if you have everything installed and updated, and help you download any extra tools you need. + +If you prefer to set things up manually, be sure to have Node.js installed first. You should be running a Node version matching the [current active LTS release](https://github.com/nodejs/Release#release-schedule) or newer for this plugin to work correctly. You can check your Node.js version by typing `node -v` in the Terminal prompt. You should also have the latest release of npm installed, npm is a separate project from Node.js and is updated frequently. If you've just installed Node.js which includes a version of npm within the installation you most likely will need to also update your npm install. To update npm, type this into your terminal: `npm install npm@latest -g` @@ -13,7 +17,7 @@ To test the plugin, or to contribute to it, you can clone this repository and bu First, you need a WordPress Environment to run the plugin on. The quickest way to get up and running is to use the provided docker setup. Just install [docker](https://www.docker.com/) on your machine and run `./bin/setup-local-env.sh`. The WordPress installation should be available at `http://localhost:8888` (username: `admin`, password: `password`). -Inside the "docker" directory, you can use any docker command to interact with your containers. +Inside the "docker" directory, you can use any docker command to interact with your containers. If this port is in use, you can override it in your `docker-compose.override.yml` file. If you're running [e2e tests](https://wordpress.org/gutenberg/handbook/reference/testing-overview/#end-to-end-testing), this change will be used correctly. Alternatively, you can use your own local WordPress environment and clone this repository right into your `wp-content/plugins` directory. @@ -27,7 +31,14 @@ You can also type `npm run package-plugin` which will run the two commands above ## Workflow -A good workflow is to work directly in this repo, branch off `master`, and submit your changes as a pull request. +A good workflow for new contributors to follow is listed below: +- Fork Gutenberg repository +- Clone forked repository +- Create new branch +- Make code changes +- Commit code changes within newly created branch +- Push branch to forked repository +- Submit Pull Request to Gutenberg repository Ideally name your branches with prefixes and descriptions, like this: `[type]/[change]`. A good prefix would be: diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ea7cd8842a25be..c989429e40c5a7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -90,3 +90,4 @@ This list is manually curated to include valuable contributions by volunteers th | | @betsela | | @fuyuko | | @msdesign21 +| @thrijith | diff --git a/LICENSE.md b/LICENSE.md index 34301d8f2ced40..1ebca723d015de 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ ### WordPress - Web publishing software - Copyright 2011-2017 by the contributors + Copyright 2011-2018 by the contributors This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/bin/includes.sh b/bin/includes.sh new file mode 100755 index 00000000000000..d0028a1c665751 --- /dev/null +++ b/bin/includes.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +## +# Ask a Yes/No question, and way for a reply. +# +# This is a general-purpose function to ask Yes/No questions in Bash, either with or without a default +# answer. It keeps repeating the question until it gets a valid answer. +# +# @param {string} prompt The question to ask the user. +# @param {string} [default] Optional. "Y" or "N", for the default option to use if none is entered. +# @param {int} [timeout] Optional. The number of seconds to wait before using the default option. +# +# @returns {bool} true if the user replies Yes, false if the user replies No. +## +ask() { + # Source: https://djm.me/ask + local timeout endtime timediff prompt default reply + + while true; do + + timeout="${3:-}" + + if [ "${2:-}" = "Y" ]; then + prompt="Y/n" + default=Y + elif [ "${2:-}" = "N" ]; then + prompt="y/N" + default=N + else + prompt="y/n" + default= + timeout= + fi + + if [ -z "$timeout" ]; then + # Ask the question (not using "read -p" as it uses stderr not stdout) + echo -en "$1 [$prompt] " + + # Read the answer (use /dev/tty in case stdin is redirected from somewhere else) + read reply /dev/null 2>&1 +} diff --git a/bin/install-docker.sh b/bin/install-docker.sh new file mode 100755 index 00000000000000..a6617407e39543 --- /dev/null +++ b/bin/install-docker.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Exit if any command fails +set -e + +# Include useful functions +. "$(dirname "$0")/includes.sh" + +# Check that Docker is installed +if ! command_exists "docker"; then + echo -e $(error_message "Docker doesn't seem to be installed. Please head on over to the Docker site to download it: $(action_format "https://www.docker.com/community-edition#/download")") + exit 1 +fi + +# Check that Docker is running +if ! docker info >/dev/null 2>&1; then + echo -e $(error_message "Docker isn't running. Please check that you've started your Docker app, and see it in your system tray.") + exit 1 +fi + +# Stop existing containers +echo -e $(status_message "Stopping Docker containers...") +docker-compose down --remove-orphans >/dev/null 2>&1 + +# Download image updates +echo -e $(status_message "Downloading Docker image updates...") +docker-compose pull --parallel + +# Launch the containers +echo -e $(status_message "Starting Docker containers...") +docker-compose up -d >/dev/null + +HOST_PORT=$(docker-compose port wordpress 80 | awk -F : '{printf $2}') + +# Wait until the docker containers are setup properely +echo -en $(status_message "Attempting to connect to wordpress...") +until $(curl -L http://localhost:$HOST_PORT -so - 2>&1 | grep -q "WordPress"); do + echo -n '.' + sleep 5 +done +echo '' + +# Install WordPress +echo -e $(status_message "Installing WordPress...") +docker-compose run --rm cli core install --url=localhost:$HOST_PORT --title=Gutenberg --admin_user=admin --admin_password=password --admin_email=test@test.com >/dev/null +# Check for WordPress updates, just in case the WordPress image isn't up to date. +docker-compose run --rm cli core update >/dev/null + +# If the 'wordpress' volume wasn't during the down/up earlier, but the post port has changed, we need to update it. +CURRENT_URL=$(docker-compose run -T --rm cli option get siteurl) +if [ "$CURRENT_URL" != "http://localhost:$HOST_PORT" ]; then + docker-compose run --rm cli option update home "http://localhost:$HOST_PORT" >/dev/null + docker-compose run --rm cli option update siteurl "http://localhost:$HOST_PORT" >/dev/null +fi + +# Activate Gutenberg +echo -e $(status_message "Activating Gutenberg...") +docker-compose run --rm cli plugin activate gutenberg >/dev/null + +# Install the PHPUnit test scaffolding +echo -e $(status_message "Installing PHPUnit test scaffolding...") +docker-compose run --rm wordpress_phpunit /app/bin/install-wp-tests.sh wordpress_test root example mysql latest false >/dev/null + +# Install Composer +echo -e $(status_message "Installing and updating Composer modules...") +docker-compose run --rm composer install diff --git a/bin/install-node-nvm.sh b/bin/install-node-nvm.sh new file mode 100755 index 00000000000000..2e37e44a648b20 --- /dev/null +++ b/bin/install-node-nvm.sh @@ -0,0 +1,90 @@ +#!/bin/bash +NVM_VERSION="v0.33.8" + +# Exit if any command fails +set -e + +# Include useful functions +. "$(dirname "$0")/includes.sh" + +# Load NVM +if [ -n "$NVM_DIR" ]; then + # The --no-use option ensures loading NVM doesn't switch the current version. + . "$NVM_DIR/nvm.sh" --no-use +fi + +# Change to the expected directory +cd "$(dirname "$0")/.." + +# Check if nvm is installed +if [ "$TRAVIS" != "true" ] && ! command_exists "nvm"; then + if ask "$(error_message "NVM isn't installed, would you like to download and install it automatically?")" Y; then + # The .bash_profile file needs to exist for NVM to install + if [ ! -e ~/.bash_profile ]; then + touch ~/.bash_profile + fi + + echo -en $(status_message "Installing NVM..." ) + download "https://raw.githubusercontent.com/creationix/nvm/$NVM_VERSION/install.sh" | bash >/dev/null 2>&1 + echo ' done!' + + echo -e $(warning_message "NVM was updated, please run this command to reload it:" ) + echo -e $(warning_message "$(action_format ". \$HOME/.nvm/nvm.sh")" ) + echo -e $(warning_message "After that, re-run the setup script to continue." ) + else + echo -e $(error_message "") + echo -e $(error_message "Please install NVM manually, then re-run the setup script to continue.") + echo -e $(error_message "NVM installation instructions can be found here: $(action_format "https://github.com/creationix/nvm")") + fi + + exit 1 +fi + +# Check if the current nvm version is up to date. +if [ "$TRAVIS" != "true" ] && [ $NVM_VERSION != "v$(nvm --version)" ]; then + echo -en $(status_message "Updating NVM..." ) + download "https://raw.githubusercontent.com/creationix/nvm/$NVM_VERSION/install.sh" | bash >/dev/null 2>&1 + echo ' done!' + + echo -e $(warning_message "NVM was updated, please run this command to reload it:" ) + echo -e $(warning_message "$(action_format ". \$HOME/.nvm/nvm.sh")" ) + echo -e $(warning_message "After that, re-run the setup script to continue." ) + exit 1 +fi + +# Check if the current node version is up to date. +if [ "$TRAVIS" != "true" ] && [ "$(nvm current)" != "$(nvm version-remote --lts)" ]; then + echo -en $(status_message "Updating Node..." ) + nvm install >/dev/null 2>&1 + echo ' done!' + + echo -e $(warning_message "A new node version was installed, please run this command to use it:" ) + echo -e $(warning_message "$(action_format "nvm use")" ) + echo -e $(warning_message "After that, re-run the setup script to continue." ) + exit 1 +fi + +# Install/update packages +echo -e $(status_message "Installing and updating NPM packages..." ) +npm install + +# There was a bug in NPM that caused changes in package-lock.json. Handle that. +if [ "$TRAVIS" != "true" ] && ! git diff --exit-code package-lock.json >/dev/null; then + if ask "$(warning_message "Your package-lock.json changed, which may mean there's an issue with your NPM cache. Would you like to try and automatically clean it up?" )" N 10; then + rm -rf node_modules/ + npm cache clean --force >/dev/null 2>&1 + git checkout package-lock.json + + echo -e $(status_message "Reinstalling NPM packages..." ) + npm install + + # Check that it's cleaned up now. + if git diff --exit-code package-lock.json >/dev/null; then + echo -e $(warning_message "Confirmed that the NPM cache is cleaned up." ) + else + echo -e $(error_message "We were unable to clean the NPM cache, please manually review the changes to package-lock.json. Continuing with the setup process..." ) + fi + else + echo -e $(warning_message "Please manually review the changes to package-lock.json. Continuing with the setup process..." ) + fi +fi diff --git a/bin/install-php-phpunit.sh b/bin/install-php-phpunit.sh old mode 100644 new mode 100755 diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh index 48ab5199a50aa0..26b5cc89cae3ea 100755 --- a/bin/install-wp-tests.sh +++ b/bin/install-wp-tests.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# Include useful functions +. "$(dirname "$0")/includes.sh" + if [ $# -lt 3 ]; then echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" exit 1 @@ -15,14 +18,6 @@ SKIP_DB_CREATE=${6-false} WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} -download() { - if [ `which curl` ]; then - curl -s "$1" > "$2"; - elif [ `which wget` ]; then - wget -nv -O "$2" "$1" - fi -} - if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then WP_TESTS_TAG="tags/$WP_VERSION" elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then diff --git a/bin/run-wp-unit-tests.sh b/bin/run-wp-unit-tests.sh index 333bbea525d8e4..1c05037780d782 100755 --- a/bin/run-wp-unit-tests.sh +++ b/bin/run-wp-unit-tests.sh @@ -8,11 +8,13 @@ if [ ${DOCKER} = "true" ]; then else bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION source bin/install-php-phpunit.sh + + # Run the build because otherwise there will be a bunch of warnings about + # failed `stat` calls from `filemtime()`. + composer install || exit 1 + npm install || exit 1 fi -# Run the build because otherwise there will be a bunch of warnings about -# failed `stat` calls from `filemtime()`. -composer install || exit 1 -npm install || exit 1 + npm run build || exit 1 # Make sure phpegjs parser is up to date @@ -27,8 +29,8 @@ fi echo Running with the following versions: if [ ${DOCKER} = "true" ]; then - docker-compose -f docker/docker-compose.yml run --rm wordpress_phpunit php -v - docker-compose -f docker/docker-compose.yml run --rm wordpress_phpunit phpunit --version + docker-compose run --rm wordpress_phpunit php -v + docker-compose run --rm wordpress_phpunit phpunit --version else php -v phpunit --version diff --git a/bin/setup-local-env.sh b/bin/setup-local-env.sh index 1fd78eddc7e03b..075f1aeb329c80 100755 --- a/bin/setup-local-env.sh +++ b/bin/setup-local-env.sh @@ -3,24 +3,28 @@ # Exit if any command fails set -e +# Include useful functions +. "$(dirname "$0")/includes.sh" + # Change to the expected directory -cd "$(dirname "$0")/../docker" +cd "$(dirname "$0")/.." -# Launch the containers -docker-compose up -d +# Check Node and NVM are installed +. "$(dirname "$0")/install-node-nvm.sh" -# Wait until the docker containers are setup properely -echo "Attempting to connect to wordpress" -until $(curl -L http://localhost:8888 -so - | grep -q "WordPress"); do - printf '.' - sleep 5 -done +# Check Docker is installed and running +. "$(dirname "$0")/install-docker.sh" -# Install WordPress -docker run -it --rm --volumes-from wordpress-dev --network container:wordpress-dev wordpress:cli core install --url=localhost:8888 --title=Gutenberg --admin_user=admin --admin_password=password --admin_email=test@test.com +! read -d '' GUTENBERG <<"EOT" +,⁻⁻⁻. . | +| _. . . |--- ,---. ,---. |---. ,---. ,---. ,---. +| | | | | |---' | | | | |---' | | | +`---' `---' `---’ `---’ ' ` `---' `---’ ` `---| + `---' +EOT -# Activate Gutenberg -docker run -it --rm --volumes-from wordpress-dev --network container:wordpress-dev wordpress:cli plugin activate gutenberg +CURRENT_URL=$(docker-compose run -T --rm cli option get siteurl) -# Install the PHPUnit test scaffolding -docker-compose run --rm wordpress_phpunit /app/bin/install-wp-tests.sh wordpress_test root example mysql latest false +echo -e "\nWelcome to...\n" +echo -e "\033[95m$GUTENBERG\033[0m" +echo -e "Run $(action_format "npm run dev"), then open $(action_format "$CURRENT_URL") to get started!" diff --git a/blocks/alignment-toolbar/index.js b/blocks/alignment-toolbar/index.js index 8ac7ad70e76138..73fac92bdd7686 100644 --- a/blocks/alignment-toolbar/index.js +++ b/blocks/alignment-toolbar/index.js @@ -23,6 +23,10 @@ const ALIGNMENT_CONTROLS = [ ]; export default function AlignmentToolbar( { value, onChange } ) { + function applyOrUnset( align ) { + return () => onChange( value === align ? undefined : align ); + } + return ( { @@ -32,7 +36,7 @@ export default function AlignmentToolbar( { value, onChange } ) { return { ...control, isActive, - onClick: () => onChange( isActive ? undefined : align ), + onClick: applyOrUnset( align ), }; } ) } /> diff --git a/blocks/api/categories.js b/blocks/api/categories.js index bf7894bbd62c3a..2a76e170cb3782 100644 --- a/blocks/api/categories.js +++ b/blocks/api/categories.js @@ -21,9 +21,9 @@ const categories = [ ]; /** - * Returns all the block categories + * Returns all the block categories. * - * @return {Array} Block categories + * @returns {Array} Block categories. */ export function getCategories() { return categories; diff --git a/blocks/api/factory.js b/blocks/api/factory.js index f3fb437c932e9a..eec7e77aa73581 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -30,9 +30,10 @@ import { getBlockType, getBlockTypes } from './registration'; /** * Returns a block object given its type and attributes. * - * @param {String} name Block name - * @param {Object} blockAttributes Block attributes - * @return {Object} Block object + * @param {string} name Block name. + * @param {Object} blockAttributes Block attributes. + * + * @returns {Object} Block object. */ export function createBlock( name, blockAttributes = {} ) { // Get the type definition associated with a registered block. @@ -62,12 +63,14 @@ export function createBlock( name, blockAttributes = {} ) { } /** - * Returns a predicate that receives a transformation and returns true if the given - * transformation is able to execute in the situation specified in the params + * Returns a predicate that receives a transformation and returns true if the + * given transformation is able to execute in the situation specified in the + * params. + * + * @param {string} sourceName Block name. + * @param {boolean} isMultiBlock Array of possible block transformations. * - * @param {String} sourceName Block name - * @param {Boolean} isMultiBlock Array of possible block transformations - * @return {Function} Predicate that receives a block type. + * @returns {Function} Predicate that receives a block type. */ const isTransformForBlockSource = ( sourceName, isMultiBlock = false ) => ( transform ) => ( transform.type === 'block' && @@ -76,12 +79,14 @@ const isTransformForBlockSource = ( sourceName, isMultiBlock = false ) => ( tran ); /** - * Returns a predicate that receives a block type and returns true if the given block type contains a - * transformation able to execute in the situation specified in the params + * Returns a predicate that receives a block type and returns true if the given + * block type contains a transformation able to execute in the situation + * specified in the params. + * + * @param {string} sourceName Block name. + * @param {boolean} isMultiBlock Array of possible block transformations. * - * @param {String} sourceName Block name - * @param {Boolean} isMultiBlock Array of possible block transformations - * @return {Function} Predicate that receives a block type. + * @returns {Function} Predicate that receives a block type. */ const createIsTypeTransformableFrom = ( sourceName, isMultiBlock = false ) => ( type ) => ( !! find( @@ -91,10 +96,12 @@ const createIsTypeTransformableFrom = ( sourceName, isMultiBlock = false ) => ( ); /** - * Returns an array of possible block transformations that could happen on the set of blocks received as argument. + * Returns an array of possible block transformations that could happen on the + * set of blocks received as argument. * - * @param {Array} blocks Blocks array - * @return {Array} Array of possible block transformations + * @param {Array} blocks Blocks array. + * + * @returns {Array} Array of possible block transformations. */ export function getPossibleBlockTransformations( blocks ) { const sourceBlock = first( blocks ); @@ -139,9 +146,10 @@ export function getPossibleBlockTransformations( blocks ) { /** * Switch one or more blocks into one or more blocks of the new block type. * - * @param {Array|Object} blocks Blocks array or block object - * @param {string} name Block name - * @return {Array} Array of blocks + * @param {Array|Object} blocks Blocks array or block object. + * @param {string} name Block name. + * + * @returns {Array} Array of blocks. */ export function switchToBlockType( blocks, name ) { const blocksArray = castArray( blocks ); @@ -216,9 +224,12 @@ export function switchToBlockType( blocks, name ) { /** * Creates a new reusable block. * - * @param {String} type The type of the block referenced by the reusable block - * @param {Object} attributes The attributes of the block referenced by the reusable block - * @return {Object} A reusable block object + * @param {string} type The type of the block referenced by the reusable + * block. + * @param {Object} attributes The attributes of the block referenced by the + * reusable block. + * + * @returns {Object} A reusable block object. */ export function createReusableBlock( type, attributes ) { return { diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 82add30a463e8f..efa58c48169c5c 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -15,13 +15,14 @@ import { getCommentDelimitedContent } from './serializer'; import { attr, prop, html, text, query, node, children } from './matchers'; /** - * Returns value coerced to the specified JSON schema type string + * Returns value coerced to the specified JSON schema type string. * * @see http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.25 * - * @param {*} value Original value - * @param {String} type Type to coerce - * @return {*} Coerced value + * @param {*} value Original value. + * @param {string} type Type to coerce. + * + * @returns {*} Coerced value. */ export function asType( value, type ) { switch ( type ) { @@ -53,10 +54,11 @@ export function asType( value, type ) { } /** - * Returns an hpq matcher given a source object + * Returns an hpq matcher given a source object. + * + * @param {Object} sourceConfig Attribute Source object. * - * @param {Object} sourceConfig Attribute Source object - * @return {Function} hpq Matcher + * @returns {Function} A hpq Matcher. */ export function matcherFromSource( sourceConfig ) { switch ( sourceConfig.source ) { @@ -82,15 +84,16 @@ export function matcherFromSource( sourceConfig ) { } /** - * Given an attribute key, an attribute's schema, a block's raw content and the commentAttributes - * returns the attribute value depending on its source definition of the given attribute key + * Given an attribute key, an attribute's schema, a block's raw content and the + * commentAttributes returns the attribute value depending on its source + * definition of the given attribute key. * - * @param {string} attributeKey Attribute key - * @param {Object} attributeSchema Attribute's schema - * @param {string} innerHTML Block's raw content - * @param {Object} commentAttributes Block's comment attributes + * @param {string} attributeKey Attribute key. + * @param {Object} attributeSchema Attribute's schema. + * @param {string} innerHTML Block's raw content. + * @param {Object} commentAttributes Block's comment attributes. * - * @return {*} Attribute value + * @returns {*} Attribute value. */ export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, commentAttributes ) { let value; @@ -116,10 +119,11 @@ export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, com /** * Returns the block attributes of a registered block node given its type. * - * @param {?Object} blockType Block type - * @param {string} innerHTML Raw block content - * @param {?Object} attributes Known block attributes (from delimiters) - * @return {Object} All block attributes + * @param {?Object} blockType Block type. + * @param {string} innerHTML Raw block content. + * @param {?Object} attributes Known block attributes (from delimiters). + * + * @returns {Object} All block attributes. */ export function getBlockAttributes( blockType, innerHTML, attributes ) { const blockAttributes = mapValues( blockType.attributes, ( attributeSchema, attributeKey ) => { @@ -130,12 +134,14 @@ export function getBlockAttributes( blockType, innerHTML, attributes ) { } /** - * Attempt to parse the innerHTML using using a supplied `deprecated` definition. + * Attempt to parse the innerHTML using using a supplied `deprecated` + * definition. + * + * @param {?Object} blockType Block type. + * @param {string} innerHTML Raw block content. + * @param {?Object} attributes Known block attributes (from delimiters). * - * @param {?Object} blockType Block type - * @param {string} innerHTML Raw block content - * @param {?Object} attributes Known block attributes (from delimiters) - * @return {Object} Block attributes + * @returns {Object} Block attributes. */ export function getAttributesFromDeprecatedVersion( blockType, innerHTML, attributes ) { if ( ! blockType.deprecated ) { @@ -147,10 +153,19 @@ export function getAttributesFromDeprecatedVersion( blockType, innerHTML, attrib ...omit( blockType, [ 'attributes', 'save', 'supports' ] ), // Parsing/Serialization properties ...blockType.deprecated[ i ], }; - const deprecatedBlockAttributes = getBlockAttributes( deprecatedBlockType, innerHTML, attributes ); - const isValid = isValidBlock( innerHTML, deprecatedBlockType, deprecatedBlockAttributes ); - if ( isValid ) { - return deprecatedBlockAttributes; + + try { + // Parse using the deprecated block version . + // Try to validate the parsed block using this same deprecated version. + // Ignore this version if the the validation fails. + const deprecatedBlockAttributes = getBlockAttributes( deprecatedBlockType, innerHTML, attributes ); + const migratedBlockAttributes = deprecatedBlockType.migrate ? deprecatedBlockType.migrate( deprecatedBlockAttributes ) : deprecatedBlockAttributes; + const isValid = isValidBlock( innerHTML, deprecatedBlockType, deprecatedBlockAttributes ); + if ( isValid ) { + return migratedBlockAttributes; + } + } catch ( error ) { + // ignore error, it means this deprecated version is invalid } } } @@ -158,10 +173,11 @@ export function getAttributesFromDeprecatedVersion( blockType, innerHTML, attrib /** * Creates a block with fallback to the unknown type handler. * - * @param {?String} name Block type name - * @param {String} innerHTML Raw block content - * @param {?Object} attributes Attributes obtained from block delimiters - * @return {?Object} An initialized block object (if possible) + * @param {?String} name Block type name. + * @param {string} innerHTML Raw block content. + * @param {?Object} attributes Attributes obtained from block delimiters. + * + * @returns {?Object} An initialized block object (if possible). */ export function createBlockWithFallback( name, innerHTML, attributes ) { // Use type from block content, otherwise find unknown handler. @@ -227,8 +243,9 @@ export function createBlockWithFallback( name, innerHTML, attributes ) { /** * Parses the post content with a PegJS grammar and returns a list of blocks. * - * @param {String} content The post content - * @return {Array} Block list + * @param {string} content The post content. + * + * @returns {Array} Block list. */ export function parseWithGrammar( content ) { return grammarParse( content ).reduce( ( memo, blockNode ) => { diff --git a/blocks/api/raw-handling/index.js b/blocks/api/raw-handling/index.js index 21c1e25cd7bbf3..7fb5ed22bbb26b 100644 --- a/blocks/api/raw-handling/index.js +++ b/blocks/api/raw-handling/index.js @@ -28,14 +28,16 @@ import shortcodeConverter from './shortcode-converter'; /** * Converts an HTML string to known blocks. Strips everything else. * - * @param {String} options.HTML The HTML to convert. - * @param {String} [options.plainText] Plain text version. - * @param {String} [options.mode] Handle content as blocks or inline content. - * * 'AUTO': Decide based on the content passed. - * * 'INLINE': Always handle as inline content, and return string. - * * 'BLOCKS': Always handle as blocks, and return array of blocks. - * @param {Array} [options.tagName] The tag into which content will be inserted. - * @return {Array|String} A list of blocks or a string, depending on `handlerMode`. + * @param {string} [options.HTML] The HTML to convert. + * @param {string} [options.plainText] Plain text version. + * @param {string} [options.mode] Handle content as blocks or inline content. + * * 'AUTO': Decide based on the content passed. + * * 'INLINE': Always handle as inline content, and return string. + * * 'BLOCKS': Always handle as blocks, and return array of blocks. + * @param {Array} [options.tagName] The tag into which content will be + * inserted. + * + * @returns {Array|String} A list of blocks or a string, depending on `handlerMode`. */ export default function rawHandler( { HTML, plainText = '', mode = 'AUTO', tagName } ) { // First of all, strip any meta tags. diff --git a/blocks/api/raw-handling/test/integration/google-docs-out.html b/blocks/api/raw-handling/test/integration/google-docs-out.html index 8cb94dc1bbcb80..d8242c4779198e 100644 --- a/blocks/api/raw-handling/test/integration/google-docs-out.html +++ b/blocks/api/raw-handling/test/integration/google-docs-out.html @@ -60,6 +60,6 @@

This is a heading

-
+
diff --git a/blocks/api/raw-handling/test/integration/ms-word-online-out.html b/blocks/api/raw-handling/test/integration/ms-word-online-out.html index 997359fa840372..d7d6370dfc3baf 100644 --- a/blocks/api/raw-handling/test/integration/ms-word-online-out.html +++ b/blocks/api/raw-handling/test/integration/ms-word-online-out.html @@ -50,5 +50,5 @@ -
+
diff --git a/blocks/api/raw-handling/test/integration/ms-word-out.html b/blocks/api/raw-handling/test/integration/ms-word-out.html index 3f4687910c6ce9..b5d2e6ba1ca548 100644 --- a/blocks/api/raw-handling/test/integration/ms-word-out.html +++ b/blocks/api/raw-handling/test/integration/ms-word-out.html @@ -85,5 +85,5 @@

This is a heading level 2

-
+
diff --git a/blocks/api/raw-handling/utils.js b/blocks/api/raw-handling/utils.js index 777c0796eaafb9..faa157c40fe6e2 100644 --- a/blocks/api/raw-handling/utils.js +++ b/blocks/api/raw-handling/utils.js @@ -82,9 +82,11 @@ export function isAttributeWhitelisted( tag, attribute ) { * Checks if nodeName should be treated as inline when being added to tagName. * This happens if nodeName and tagName are in the same group defined in inlineWhitelistTagGroups. * - * @param {String} nodeName Node name. - * @param {String} tagName Tag name. - * @return {Boolean} True if nodeName is inline in the context of tagName and false otherwise. + * @param {string} nodeName Node name. + * @param {string} tagName Tag name. + * + * @returns {boolean} True if nodeName is inline in the context of tagName and + * false otherwise. */ function isInlineForTag( nodeName, tagName ) { if ( ! tagName || ! nodeName ) { @@ -195,9 +197,9 @@ export function isPlain( HTML ) { /** * Given node filters, deeply filters and mutates a NodeList. * - * @param {NodeList} nodeList The nodeList to filter. - * @param {Array} filters An array of functions that can mutate with the provided node. - * @param {Document} doc The document of the nodeList. + * @param {NodeList} nodeList The nodeList to filter. + * @param {Array} filters An array of functions that can mutate with the provided node. + * @param {Document} doc The document of the nodeList. */ export function deepFilterNodeList( nodeList, filters, doc ) { Array.from( nodeList ).forEach( ( node ) => { @@ -217,9 +219,10 @@ export function deepFilterNodeList( nodeList, filters, doc ) { /** * Given node filters, deeply filters HTML tags. * - * @param {String} HTML The HTML to filter. - * @param {Array} filters An array of functions that can mutate with the provided node. - * @return {String} The filtered HTML. + * @param {string} HTML The HTML to filter. + * @param {Array} filters An array of functions that can mutate with the provided node. + * + * @returns {string} The filtered HTML. */ export function deepFilterHTML( HTML, filters = [] ) { const doc = document.implementation.createHTMLDocument( '' ); diff --git a/blocks/api/registration.js b/blocks/api/registration.js index 22ad06b0e5a770..80860c8fc35b49 100644 --- a/blocks/api/registration.js +++ b/blocks/api/registration.js @@ -43,10 +43,11 @@ let defaultBlockName; * behavior. Once registered, the block is made available as an option to any * editor interface where blocks are implemented. * - * @param {string} name Block name - * @param {Object} settings Block settings - * @return {?WPBlock} The block, if it has been successfully - * registered; otherwise `undefined`. + * @param {string} name Block name. + * @param {Object} settings Block settings. + * + * @returns {?WPBlock} The block, if it has been successfully registered; + * otherwise `undefined`. */ export function registerBlockType( name, settings ) { settings = { @@ -127,9 +128,10 @@ export function registerBlockType( name, settings ) { /** * Unregisters a block. * - * @param {string} name Block name - * @return {?WPBlock} The previous block value, if it has been - * successfully unregistered; otherwise `undefined`. + * @param {string} name Block name. + * + * @returns {?WPBlock} The previous block value, if it has been successfully + * unregistered; otherwise `undefined`. */ export function unregisterBlockType( name ) { if ( ! blocks[ name ] ) { @@ -146,7 +148,7 @@ export function unregisterBlockType( name ) { /** * Assigns name of block handling unknown block types. * - * @param {string} name Block name + * @param {string} name Block name. */ export function setUnknownTypeHandlerName( name ) { unknownTypeHandlerName = name; @@ -156,25 +158,25 @@ export function setUnknownTypeHandlerName( name ) { * Retrieves name of block handling unknown block types, or undefined if no * handler has been defined. * - * @return {?string} Blog name + * @returns {?string} Blog name. */ export function getUnknownTypeHandlerName() { return unknownTypeHandlerName; } /** - * Assigns the default block name + * Assigns the default block name. * - * @param {string} name Block name + * @param {string} name Block name. */ export function setDefaultBlockName( name ) { defaultBlockName = name; } /** - * Retrieves the default block name + * Retrieves the default block name. * - * @return {?string} Blog name + * @returns {?string} Blog name. */ export function getDefaultBlockName() { return defaultBlockName; @@ -183,8 +185,9 @@ export function getDefaultBlockName() { /** * Returns a registered block type. * - * @param {string} name Block name - * @return {?Object} Block type + * @param {string} name Block name. + * + * @returns {?Object} Block type. */ export function getBlockType( name ) { return blocks[ name ]; @@ -193,20 +196,21 @@ export function getBlockType( name ) { /** * Returns all registered blocks. * - * @return {Array} Block settings + * @returns {Array} Block settings. */ export function getBlockTypes() { return Object.values( blocks ); } /** - * Returns true if the block defines support for a feature, or false otherwise + * Returns true if the block defines support for a feature, or false otherwise. * - * @param {(String|Object)} nameOrType Block name or type object - * @param {String} feature Feature to test - * @param {Boolean} defaultSupports Whether feature is supported by - * default if not explicitly defined - * @return {Boolean} Whether block supports feature + * @param {(string|Object)} nameOrType Block name or type object. + * @param {string} feature Feature to test. + * @param {boolean} defaultSupports Whether feature is supported by + * default if not explicitly defined. + * + * @returns {boolean} Whether block supports feature. */ export function hasBlockSupport( nameOrType, feature, defaultSupports ) { const blockType = 'string' === typeof nameOrType ? @@ -221,11 +225,12 @@ export function hasBlockSupport( nameOrType, feature, defaultSupports ) { /** * Determines whether or not the given block is a reusable block. This is a - * special block type that is used to point to a global block stored via the - * API. + * special block type that is used to point to a global block stored via + * the API. + * + * @param {Object} blockOrType Block or Block Type to test. * - * @param {Object} blockOrType Block or Block Type to test - * @return {Boolean} Whether the given block is a reusable block + * @returns {boolean} Whether the given block is a reusable block. */ export function isReusableBlock( blockOrType ) { return blockOrType.name === 'core/block'; diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index 24b903997c3fc4..da93b99f2bf735 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -16,10 +16,11 @@ import { applyFilters } from '@wordpress/hooks'; import { getBlockType, getUnknownTypeHandlerName } from './registration'; /** - * Returns the block's default classname from its name + * Returns the block's default classname from its name. * - * @param {String} blockName The block name - * @return {string} The block's default class + * @param {string} blockName The block name. + * + * @returns {string} The block's default class. */ export function getBlockDefaultClassname( blockName ) { // Drop common prefixes: 'core/' or 'core-' (in 'core-embed/') @@ -30,9 +31,10 @@ export function getBlockDefaultClassname( blockName ) { * Given a block type containg a save render implementation and attributes, returns the * enhanced element to be saved or string when raw HTML expected. * - * @param {Object} blockType Block type - * @param {Object} attributes Block attributes - * @return {Object|string} Save content + * @param {Object} blockType Block type. + * @param {Object} attributes Block attributes. + * + * @returns {Object|string} Save content. */ export function getSaveElement( blockType, attributes ) { const { save } = blockType; @@ -68,9 +70,10 @@ export function getSaveElement( blockType, attributes ) { * Given a block type containg a save render implementation and attributes, returns the * static markup to be saved. * - * @param {Object} blockType Block type - * @param {Object} attributes Block attributes - * @return {string} Save content + * @param {Object} blockType Block type. + * @param {Object} attributes Block attributes. + * + * @returns {string} Save content. */ export function getSaveContent( blockType, attributes ) { const saveElement = getSaveElement( blockType, attributes ); @@ -95,9 +98,10 @@ export function getSaveContent( blockType, attributes ) { * This function returns only those attributes which are needed to persist and * which cannot be matched from the block content. * - * @param {Object} allAttributes Attributes from in-memory block data - * @param {Object} blockType Block type - * @returns {Object} Subset of attributes for comment serialization + * @param {Object} allAttributes Attributes from in-memory block data. + * @param {Object} blockType Block type. + * + * @returns {Object} Subset of attributes for comment serialization. */ export function getCommentAttributes( allAttributes, blockType ) { const attributes = reduce( blockType.attributes, ( result, attributeSchema, key ) => { @@ -139,8 +143,9 @@ export function serializeAttributes( attrs ) { * Returns HTML markup processed by a markup beautifier configured for use in * block serialization. * - * @param {String} content Original HTML - * @return {String} Beautiful HTML + * @param {string} content Original HTML. + * + * @returns {string} Beautiful HTML. */ export function getBeautifulContent( content ) { return beautifyHtml( content, { @@ -150,9 +155,11 @@ export function getBeautifulContent( content ) { } /** - * Given a block object, returns the Block's Inner HTML markup - * @param {Object} block Block Object - * @return {String} HTML + * Given a block object, returns the Block's Inner HTML markup. + * + * @param {Object} block Block Object. + * + * @returns {string} HTML. */ export function getBlockContent( block ) { const blockType = getBlockType( block.name ); @@ -172,10 +179,11 @@ export function getBlockContent( block ) { /** * Returns the content of a block, including comment delimiters. * - * @param {String} rawBlockName Block name - * @param {Object} attributes Block attributes - * @param {String} content Block save content - * @return {String} Comment-delimited block content + * @param {string} rawBlockName Block name. + * @param {Object} attributes Block attributes. + * @param {string} content Block save content. + * + * @returns {string} Comment-delimited block content. */ export function getCommentDelimitedContent( rawBlockName, attributes, content ) { const serializedAttributes = ! isEmpty( attributes ) ? @@ -202,8 +210,9 @@ export function getCommentDelimitedContent( rawBlockName, attributes, content ) * Returns the content of a block, including comment delimiters, determining * serialized attributes and content form from the current state of the block. * - * @param {Object} block Block instance - * @return {String} Serialized block + * @param {Object} block Block instance. + * + * @returns {string} Serialized block. */ export function serializeBlock( block ) { const blockName = block.name; @@ -236,8 +245,9 @@ export function serializeBlock( block ) { /** * Takes a block or set of blocks and returns the serialized post content. * - * @param {Array} blocks Block(s) to serialize - * @return {String} The post content + * @param {Array} blocks Block(s) to serialize. + * + * @returns {string} The post content. */ export default function serialize( blocks ) { return castArray( blocks ).map( serializeBlock ).join( '\n\n' ); diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 709ed53744aede..9359ad4d00aae3 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -296,6 +296,7 @@ describe( 'block parser', () => { }, }, save: ( { attributes } ) => { attributes.fruit }, + migrate: ( attributes ) => ( { fruit: 'Big ' + attributes.fruit } ), }, ], } ); @@ -306,7 +307,7 @@ describe( 'block parser', () => { { fruit: 'Bananas' } ); expect( block.name ).toEqual( 'core/test-block' ); - expect( block.attributes ).toEqual( { fruit: 'Bananas' } ); + expect( block.attributes ).toEqual( { fruit: 'Big Bananas' } ); expect( block.isValid ).toBe( true ); expect( console ).toHaveErrored(); expect( console ).toHaveWarned(); diff --git a/blocks/api/validation.js b/blocks/api/validation.js index 2155bf3a707864..bcf33ca810158d 100644 --- a/blocks/api/validation.js +++ b/blocks/api/validation.js @@ -132,8 +132,9 @@ const log = ( () => { /** * Creates a logger with block validation prefix. * - * @param {Function} logger Original logger function - * @return {Function} Augmented logger function + * @param {Function} logger Original logger function. + * + * @returns {Function} Augmented logger function. */ function createLogger( logger ) { // In test environments, pre-process the sprintf message to improve @@ -159,8 +160,9 @@ const log = ( () => { * Given a specified string, returns an array of strings split by consecutive * whitespace, ignoring leading or trailing whitespace. * - * @param {String} text Original text - * @return {String[]} Text pieces split on whitespace + * @param {string} text Original text. + * + * @returns {String[]} Text pieces split on whitespace. */ export function getTextPiecesSplitOnWhitespace( text ) { return text.trim().split( REGEXP_WHITESPACE ); @@ -170,8 +172,9 @@ export function getTextPiecesSplitOnWhitespace( text ) { * Given a specified string, returns a new trimmed string where all consecutive * whitespace is collapsed to a single space. * - * @param {String} text Original text - * @return {String} Trimmed text with consecutive whitespace collapsed + * @param {string} text Original text. + * + * @returns {string} Trimmed text with consecutive whitespace collapsed. */ export function getTextWithCollapsedWhitespace( text ) { return getTextPiecesSplitOnWhitespace( text ).join( ' ' ); @@ -184,8 +187,9 @@ export function getTextWithCollapsedWhitespace( text ) { * * @see MEANINGFUL_ATTRIBUTES * - * @param {Object} token StartTag token - * @return {Array[]} Attribute pairs + * @param {Object} token StartTag token. + * + * @returns {Array[]} Attribute pairs. */ export function getMeaningfulAttributePairs( token ) { return token.attributes.filter( ( pair ) => { @@ -202,9 +206,10 @@ export function getMeaningfulAttributePairs( token ) { * Returns true if two text tokens (with `chars` property) are equivalent, or * false otherwise. * - * @param {Object} actual Actual token - * @param {Object} expected Expected token - * @return {Boolean} Whether two text tokens are equivalent + * @param {Object} actual Actual token. + * @param {Object} expected Expected token. + * + * @returns {boolean} Whether two text tokens are equivalent. */ export function isEqualTextTokensWithCollapsedWhitespace( actual, expected ) { // This is an overly simplified whitespace comparison. The specification is @@ -224,8 +229,9 @@ export function isEqualTextTokensWithCollapsedWhitespace( actual, expected ) { * Given a style value, returns a normalized style value for strict equality * comparison. * - * @param {String} value Style value - * @return {String} Normalized style value + * @param {string} value Style value. + * + * @returns {string} Normalized style value. */ export function getNormalizedStyleValue( value ) { return value @@ -236,8 +242,9 @@ export function getNormalizedStyleValue( value ) { /** * Given a style attribute string, returns an object of style properties. * - * @param {String} text Style attribute - * @return {Object} Style properties + * @param {string} text Style attribute. + * + * @returns {Object} Style properties. */ export function getStyleProperties( text ) { const pairs = text @@ -278,11 +285,12 @@ export const isEqualAttributesOfName = { /** * Given two sets of attribute tuples, returns true if the attribute sets are - * equivalent + * equivalent. + * + * @param {Array[]} actual Actual attributes tuples. + * @param {Array[]} expected Expected attributes tuples. * - * @param {Array[]} actual Actual attributes tuples - * @param {Array[]} expected Expected attributes tuples - * @return {Boolean} Whether attributes are equivalent + * @returns {boolean} Whether attributes are equivalent. */ export function isEqualTagAttributePairs( actual, expected ) { // Attributes is tokenized as tuples. Their lengths should match. This also @@ -349,8 +357,9 @@ export const isEqualTokensOfType = { * * Mutates the tokens array. * - * @param {Object[]} tokens Set of tokens to search - * @return {Object} Next non-whitespace token + * @param {Object[]} tokens Set of tokens to search. + * + * @returns {Object} Next non-whitespace token. */ export function getNextNonWhitespaceToken( tokens ) { let token; @@ -369,9 +378,10 @@ export function getNextNonWhitespaceToken( tokens ) { * Returns true if there is given HTML strings are effectively equivalent, or * false otherwise. * - * @param {String} actual Actual HTML string - * @param {String} expected Expected HTML string - * @return {Boolean} Whether HTML strings are equivalent + * @param {string} actual Actual HTML string. + * @param {string} expected Expected HTML string. + * + * @returns {boolean} Whether HTML strings are equivalent. */ export function isEquivalentHTML( actual, expected ) { // Tokenize input content and reserialized save content @@ -418,10 +428,11 @@ export function isEquivalentHTML( actual, expected ) { * * Logs to console in development environments when invalid. * - * @param {String} innerHTML Original block content - * @param {String} blockType Block type - * @param {Object} attributes Parsed block attributes - * @return {Boolean} Whether block is valid + * @param {string} innerHTML Original block content. + * @param {string} blockType Block type. + * @param {Object} attributes Parsed block attributes. + * + * @returns {boolean} Whether block is valid. */ export function isValidBlock( innerHTML, blockType, attributes ) { let saveContent; diff --git a/blocks/autocompleters/index.js b/blocks/autocompleters/index.js index 4be6140eceb8e5..8775e8a63c64ab 100644 --- a/blocks/autocompleters/index.js +++ b/blocks/autocompleters/index.js @@ -19,12 +19,14 @@ import BlockIcon from '../block-icon'; /** * @callback FnGetOptions + * * @returns {Promise.>} A promise that resolves to the list of completer options. */ /** * @callback FnAllowNode * @param {Node} textNode check if the completer can handle this text node. + * * @returns {boolean} true if the completer can handle this text node. */ @@ -32,6 +34,7 @@ import BlockIcon from '../block-icon'; * @callback FnAllowContext * @param {Range} before the range before the auto complete trigger and query. * @param {Range} after the range after the autocomplete trigger and query. + * * @returns {boolean} true if the completer can handle these ranges. */ @@ -40,6 +43,7 @@ import BlockIcon from '../block-icon'; * @param {*} value the value of the completer option. * @param {Range} range the nodes included in the autocomplete trigger and query. * @param {String} query the text value of the autocomplete query. + * * @returns {?Component} optional html to replace the range. */ @@ -57,8 +61,9 @@ import BlockIcon from '../block-icon'; * Returns an "completer" definition for selecting from available blocks to replace the current one. * The definition can be understood by the Autocomplete component. * - * @param {Function} onReplace Callback to replace the current block. - * @returns {Completer} Completer object used by the Autocomplete component. + * @param {Function} onReplace Callback to replace the current block. + * + * @returns {Completer} Completer object used by the Autocomplete component. */ export function blockAutocompleter( { onReplace } ) { // Prioritize common category in block type options @@ -96,7 +101,7 @@ export function blockAutocompleter( { onReplace } ) { }; } /** - * Returns a "completer" definition for inserting links to the posts of a user. + * Returns a "completer" definition for inserting a user mention. * The definition can be understood by the Autocomplete component. * * @returns {Completer} Completer object used by the Autocomplete component. @@ -118,12 +123,12 @@ export function userAutocompleter() { } ); }; - const allowNode = ( textNode ) => { - return textNode.parentElement.closest( 'a' ) === null; + const allowNode = () => { + return true; }; const onSelect = ( user ) => { - return { '@' + user.name }; + return ( '@' + user.slug ); }; return { diff --git a/blocks/color-palette/index.js b/blocks/color-palette/index.js index 02d1beabc3d716..be57fa67dd023f 100644 --- a/blocks/color-palette/index.js +++ b/blocks/color-palette/index.js @@ -18,6 +18,10 @@ import './style.scss'; export function ColorPalette( { defaultColors, colors, value, onChange } ) { const usedColors = colors || defaultColors; + function applyOrUnset( color ) { + return () => onChange( value === color ? undefined : color ); + } + return (
{ usedColors.map( ( color ) => { @@ -30,7 +34,7 @@ export function ColorPalette( { defaultColors, colors, value, onChange } ) { type="button" className={ className } style={ style } - onClick={ () => onChange( value === color ? undefined : color ) } + onClick={ applyOrUnset( color ) } aria-label={ sprintf( __( 'Color: %s' ), color ) } aria-pressed={ value === color } /> diff --git a/blocks/color-palette/style.scss b/blocks/color-palette/style.scss index efd709f31fa9a5..98ef88cd502807 100644 --- a/blocks/color-palette/style.scss +++ b/blocks/color-palette/style.scss @@ -22,6 +22,12 @@ $color-palette-circle-spacing: 14px; &:hover { transform: scale( 1.2 ); } + + // Ensure that the
that wraps our toggle button with is full height + & > div { + height: 100%; + width: 100%; + } } .blocks-color-palette__item { diff --git a/blocks/editable/format-toolbar/index.js b/blocks/editable/format-toolbar/index.js index 49a45ab9f8f376..af0d89b4ef3f9f 100644 --- a/blocks/editable/format-toolbar/index.js +++ b/blocks/editable/format-toolbar/index.js @@ -12,7 +12,6 @@ import { keycodes } from '@wordpress/utils'; import './style.scss'; import UrlInput from '../../url-input'; import { filterURLForDisplay } from '../../../editor/utils/url'; -import ToggleControl from '../../inspector-controls/toggle-control'; const { ESCAPE, LEFT, RIGHT, UP, DOWN } = keycodes; @@ -48,11 +47,10 @@ const stopKeyPropagation = ( event ) => event.stopPropagation(); class FormatToolbar extends Component { constructor() { super( ...arguments ); + this.state = { isAddingLink: false, isEditingLink: false, - settingsVisible: false, - opensInNewWindow: false, newLinkValue: '', }; @@ -62,8 +60,6 @@ class FormatToolbar extends Component { this.submitLink = this.submitLink.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); this.onChangeLinkValue = this.onChangeLinkValue.bind( this ); - this.toggleLinkSettingsVisibility = this.toggleLinkSettingsVisibility.bind( this ); - this.setLinkTarget = this.setLinkTarget.bind( this ); } onKeyDown( event ) { @@ -83,8 +79,6 @@ class FormatToolbar extends Component { this.setState( { isAddingLink: false, isEditingLink: false, - settingsVisible: false, - opensInNewWindow: !! nextProps.formats.link && !! nextProps.formats.link.target, newLinkValue: '', } ); } @@ -102,16 +96,6 @@ class FormatToolbar extends Component { }; } - toggleLinkSettingsVisibility() { - this.setState( ( state ) => ( { settingsVisible: ! state.settingsVisible } ) ); - } - - setLinkTarget( event ) { - const opensInNewWindow = event.target.checked; - this.setState( { opensInNewWindow } ); - this.props.onChange( { link: { value: this.props.formats.link.value, target: opensInNewWindow ? '_blank' : '' } } ); - } - addLink() { this.setState( { isEditingLink: false, isAddingLink: true, newLinkValue: '' } ); } @@ -128,7 +112,7 @@ class FormatToolbar extends Component { submitLink( event ) { event.preventDefault(); - this.props.onChange( { link: { value: this.state.newLinkValue, target: this.state.opensInNewWindow ? '_blank' : '' } } ); + this.props.onChange( { link: { value: this.state.newLinkValue } } ); if ( this.state.isAddingLink ) { this.props.speak( __( 'Link added.' ), 'assertive' ); } @@ -140,7 +124,7 @@ class FormatToolbar extends Component { render() { const { formats, focusPosition, enabledControls = DEFAULT_CONTROLS, customControls = [] } = this.props; - const { isAddingLink, isEditingLink, newLinkValue, settingsVisible, opensInNewWindow } = this.state; + const { isAddingLink, isEditingLink, newLinkValue } = this.state; const linkStyle = focusPosition ? { position: 'absolute', ...focusPosition } : null; @@ -156,15 +140,6 @@ class FormatToolbar extends Component { }; } ); - const linkSettings = settingsVisible && ( -
- -
- ); - return (
@@ -183,13 +158,7 @@ class FormatToolbar extends Component { -
- { linkSettings } /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ @@ -214,13 +183,7 @@ class FormatToolbar extends Component { -
- { linkSettings }
/* eslint-enable jsx-a11y/no-static-element-interactions */ diff --git a/blocks/editable/format-toolbar/style.scss b/blocks/editable/format-toolbar/style.scss index a45cf560dcb35b..609255d2e3ffc8 100644 --- a/blocks/editable/format-toolbar/style.scss +++ b/blocks/editable/format-toolbar/style.scss @@ -7,17 +7,13 @@ box-shadow: 0px 3px 20px rgba( 18, 24, 30, .1 ), 0px 1px 3px rgba( 18, 24, 30, .1 ); border: 1px solid #e0e5e9; background: #fff; - width: 305px; + width: 300px; display: flex; flex-direction: column; font-family: $default-font; font-size: $default-font-size; line-height: $default-line-height; z-index: z-index( '.blocks-format-toolbar__link-modal' ); - - .blocks-url-input { - width: auto; - } } .blocks-format-toolbar__link-modal-line { @@ -46,15 +42,3 @@ @include long-content-fade( $size: 40% ); } } - -.blocks-format-toolbar__link-settings { - padding: 7px 8px; - border-top: 1px solid $light-gray-500; - padding-top: 8px; // add 1px for the border - - .blocks-base-control { - margin: 0; - flex-grow: 1; - flex-shrink: 1; - } -} diff --git a/blocks/editable/index.js b/blocks/editable/index.js index 552b10583d64a6..f256a7ce9fa5ea 100644 --- a/blocks/editable/index.js +++ b/blocks/editable/index.js @@ -63,7 +63,7 @@ function getFormatProperties( formatName, parents ) { switch ( formatName ) { case 'link' : { const anchor = find( parents, node => node.nodeName.toLowerCase() === 'a' ); - return !! anchor ? { value: anchor.getAttribute( 'href' ) || '', target: anchor.getAttribute( 'target' ) || '', node: anchor } : {}; + return !! anchor ? { value: anchor.getAttribute( 'href' ) || '', node: anchor } : {}; } default: return {}; @@ -126,7 +126,7 @@ export default class Editable extends Component { } /** - * Handles the onSetup event for the tinyMCE component + * Handles the onSetup event for the tinyMCE component. * * Will setup event handlers for the tinyMCE instance. * An `onSetup` function in the props will be called if it is present. @@ -160,15 +160,15 @@ export default class Editable extends Component { } /** - * Allows prop event handlers to handle an event + * Allows prop event handlers to handle an event. * - * Allow props an opportunity to handle the event, before default - * Editable behavior takes effect. Should the event be handled by a - * prop, it should `stopImmediatePropagation` on the event to stop - * continued event handling. + * Allow props an opportunity to handle the event, before default Editable + * behavior takes effect. Should the event be handled by a prop, it should + * `stopImmediatePropagation` on the event to stop continued event handling. * * @param {string} name The name of the event. - * @returns {void} + * + * @returns {void} Void. */ proxyPropHandler( name ) { return ( event ) => { @@ -253,7 +253,7 @@ export default class Editable extends Component { } /** - * Handles an undo event from tinyMCE + * Handles an undo event from tinyMCE. * * When user attempts Undo when empty Undo stack, propagate undo * action to context handler. The compromise here is that: TinyMCE @@ -276,7 +276,7 @@ export default class Editable extends Component { } /** - * Handles a paste event from tinyMCE + * Handles a paste event from tinyMCE. * * Saves the pasted data as plain text in `pastedPlainText`. * @@ -319,12 +319,14 @@ export default class Editable extends Component { } /** - * Handles a PrePasteProcess event from tinyMCE + * Handles a PrePasteProcess event from tinyMCE. * * Will call the paste handler with the pasted data. If it is a string tries - * to put it in the containing tinyMCE editor. Otherwise call the `onSplit` handler. + * to put it in the containing tinyMCE editor. Otherwise call the `onSplit` + * handler. * - * @param {PrePasteProcessEvent} event The PrePasteProcess event as triggered by tinyMCE. + * @param {PrePasteProcessEvent} event The PrePasteProcess event as triggered + * by tinyMCE. */ onPastePreProcess( event ) { const HTML = this.isPlainTextPaste ? this.pastedPlainText : event.content; @@ -519,7 +521,7 @@ export default class Editable extends Component { } /** - * Handles a keydown event from tinyMCE + * Handles a keydown event from tinyMCE. * * @param {KeydownEvent} event The keydow event as triggered by tinyMCE. */ @@ -548,7 +550,6 @@ export default class Editable extends Component { } event.preventDefault(); - event.stopImmediatePropagation(); } // If we click shift+Enter on inline Editables, we avoid creating two contenteditables @@ -586,7 +587,6 @@ export default class Editable extends Component { if ( event.shiftKey || ! this.props.onSplit ) { this.editor.execCommand( 'InsertLineBreak', false, event ); } else { - event.stopImmediatePropagation(); this.splitContent(); } } @@ -594,7 +594,7 @@ export default class Editable extends Component { } /** - * Handles tinyMCE key up event + * Handles tinyMCE key up event. * * @param {number} keyCode The key code that has been pressed on the keyboard. */ @@ -721,7 +721,7 @@ export default class Editable extends Component { } content = renderToString( content ); - this.editor.setContent( content, { format: 'raw' } ); + this.editor.setContent( content ); } getContent() { @@ -804,7 +804,7 @@ export default class Editable extends Component { if ( ! anchor ) { this.removeFormat( 'link' ); } - this.applyFormat( 'link', { href: formatValue.value, target: formatValue.target }, anchor ); + this.applyFormat( 'link', { href: formatValue.value }, anchor ); } else { this.editor.execCommand( 'Unlink' ); } diff --git a/blocks/editable/patterns.js b/blocks/editable/patterns.js index ec86fff0d1a4d9..0ca2292ef5eb4e 100644 --- a/blocks/editable/patterns.js +++ b/blocks/editable/patterns.js @@ -21,6 +21,16 @@ const { setTimeout } = window; const { ESCAPE, ENTER, SPACE, BACKSPACE } = keycodes; +/** + * Sets a timeout and checks if the given editor still exists. + * + * @param {Editor} editor TinyMCE editor instance. + * @param {Function} callback The function to call. + */ +function setSafeTimeout( editor, callback ) { + setTimeout( () => ! editor.removed && callback() ); +} + export default function( editor ) { const getContent = this.getContent.bind( this ); const { onReplace } = this.props; @@ -64,9 +74,9 @@ export default function( editor ) { enter(); // Wait for the browser to insert the character. } else if ( keyCode === SPACE ) { - setTimeout( () => searchFirstText( spacePatterns ) ); + setSafeTimeout( editor, () => searchFirstText( spacePatterns ) ); } else if ( keyCode > 47 && ! ( keyCode >= 91 && keyCode <= 93 ) ) { - setTimeout( inline ); + setSafeTimeout( editor, inline ); } }, true ); diff --git a/blocks/hooks/anchor.js b/blocks/hooks/anchor.js index 161ab2fee14966..ae284ed97f1580 100644 --- a/blocks/hooks/anchor.js +++ b/blocks/hooks/anchor.js @@ -25,10 +25,11 @@ const ANCHOR_REGEX = /[\s#]/g; /** * Filters registered block settings, extending attributes with anchor using ID - * of the first node + * of the first node. * - * @param {Object} settings Original block settings - * @return {Object} Filtered block settings + * @param {Object} settings Original block settings. + * + * @returns {Object} Filtered block settings. */ export function addAttribute( settings ) { if ( hasBlockSupport( settings, 'anchor' ) ) { @@ -50,8 +51,9 @@ export function addAttribute( settings ) { * Override the default edit UI to include a new block inspector control for * assigning the anchor ID, if block supports anchor. * - * @param {function|Component} BlockEdit Original component - * @return {function} Wrapped component + * @param {function|Component} BlockEdit Original component. + * + * @returns {string} Wrapped component. */ export function withInspectorControl( BlockEdit ) { const WrappedBlockEdit = ( props ) => { @@ -83,10 +85,11 @@ export function withInspectorControl( BlockEdit ) { * supports anchor. This is only applied if the block's save result is an * element and not a markup string. * - * @param {Object} extraProps Additional props applied to save element - * @param {Object} blockType Block type - * @param {Object} attributes Current block attributes - * @return {Object} Filtered props applied to save element + * @param {Object} extraProps Additional props applied to save element. + * @param {Object} blockType Block type. + * @param {Object} attributes Current block attributes. + * + * @returns {Object} Filtered props applied to save element. */ export function addSaveProps( extraProps, blockType, attributes ) { if ( hasBlockSupport( blockType, 'anchor' ) ) { diff --git a/blocks/hooks/custom-class-name.js b/blocks/hooks/custom-class-name.js index 3b9585a57388ff..29de5ca9338951 100644 --- a/blocks/hooks/custom-class-name.js +++ b/blocks/hooks/custom-class-name.js @@ -19,10 +19,11 @@ import InspectorControls from '../inspector-controls'; /** * Filters registered block settings, extending attributes with anchor using ID - * of the first node + * of the first node. * - * @param {Object} settings Original block settings - * @return {Object} Filtered block settings + * @param {Object} settings Original block settings. + * + * @returns {Object} Filtered block settings. */ export function addAttribute( settings ) { if ( hasBlockSupport( settings, 'customClassName', true ) ) { @@ -41,8 +42,9 @@ export function addAttribute( settings ) { * Override the default edit UI to include a new block inspector control for * assigning the custom class name, if block supports custom class name. * - * @param {function|Component} BlockEdit Original component - * @return {function} Wrapped component + * @param {function|Component} BlockEdit Original component. + * + * @returns {string} Wrapped component. */ export function withInspectorControl( BlockEdit ) { const WrappedBlockEdit = ( props ) => { @@ -73,10 +75,11 @@ export function withInspectorControl( BlockEdit ) { * supports anchor. This is only applied if the block's save result is an * element and not a markup string. * - * @param {Object} extraProps Additional props applied to save element - * @param {Object} blockType Block type - * @param {Object} attributes Current block attributes - * @return {Object} Filtered props applied to save element + * @param {Object} extraProps Additional props applied to save element. + * @param {Object} blockType Block type. + * @param {Object} attributes Current block attributes. + * + * @returns {Object} Filtered props applied to save element. */ export function addSaveProps( extraProps, blockType, attributes ) { if ( hasBlockSupport( blockType, 'customClassName', true ) && attributes.className ) { diff --git a/blocks/hooks/generated-class-name.js b/blocks/hooks/generated-class-name.js index 6a9740b3eb9248..28083b591add28 100644 --- a/blocks/hooks/generated-class-name.js +++ b/blocks/hooks/generated-class-name.js @@ -14,13 +14,14 @@ import { addFilter } from '@wordpress/hooks'; import { hasBlockSupport, getBlockDefaultClassname } from '../api'; /** - * Override props assigned to save component to inject generated className if block - * supports it. This is only applied if the block's save result is an + * Override props assigned to save component to inject generated className if + * block supports it. This is only applied if the block's save result is an * element and not a markup string. * - * @param {Object} extraProps Additional props applied to save element - * @param {Object} blockType Block type - * @return {Object} Filtered props applied to save element + * @param {Object} extraProps Additional props applied to save element. + * @param {Object} blockType Block type. + * + * @returns {Object} Filtered props applied to save element. */ export function addGeneratedClassName( extraProps, blockType ) { // Adding the generated className diff --git a/blocks/hooks/matchers.js b/blocks/hooks/matchers.js index 374411d39810e1..3dbc88ff66a977 100644 --- a/blocks/hooks/matchers.js +++ b/blocks/hooks/matchers.js @@ -76,10 +76,11 @@ export const node = ( selector ) => () => { }; /** - * Resolve the matchers attributes for backwards compatibilty + * Resolve the matchers attributes for backwards compatibilty. * - * @param {Object} settings Original block settings - * @return {Object} Filtered block settings + * @param {Object} settings Original block settings. + * + * @returns {Object} Filtered block settings. */ export function resolveAttributes( settings ) { // Resolve deprecated attributes diff --git a/blocks/image-placeholder/index.js b/blocks/image-placeholder/index.js index e28a13d8ece63b..5a92827f0c8dc3 100644 --- a/blocks/image-placeholder/index.js +++ b/blocks/image-placeholder/index.js @@ -1,24 +1,36 @@ +/** + * External dependencies + */ +import { map } from 'lodash'; + /** * WordPress dependencies */ -import { DropZone, FormFileUpload, Placeholder } from '@wordpress/components'; +import { DropZone, FormFileUpload, Placeholder, Button } from '@wordpress/components'; import { mediaUpload } from '@wordpress/utils'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import MediaUploadButton from '../media-upload-button'; +import MediaUpload from '../media-upload'; +import { rawHandler } from '../api'; /** * ImagePlaceHolder is a react component used by blocks containing user configurable images e.g: image and cover image. * * @param {Object} props React props passed to the component. - * @returns {Object} Rendered placeholder. + * + * @returns {Object} Rendered placeholder. */ export default function ImagePlaceHolder( { className, icon, label, onSelectImage } ) { const setImage = ( [ image ] ) => onSelectImage( image ); - const dropFiles = ( files ) => mediaUpload( files, setImage ); + const onFilesDrop = ( files ) => mediaUpload( files, setImage ); + const onHTMLDrop = ( HTML ) => setImage( map( + rawHandler( { HTML, mode: 'BLOCKS' } ) + .filter( ( { name } ) => name === 'core/image' ), + 'attributes' + ) ); const uploadFromFiles = ( event ) => mediaUpload( event.target.files, setImage ); return ( { __( 'Upload' ) } - - { __( 'Add from Media Library' ) } - + render={ ( { open } ) => ( + + ) } + /> ); } diff --git a/blocks/index.js b/blocks/index.js index 7e069d0f030de0..7c3ffb4f196ee5 100644 --- a/blocks/index.js +++ b/blocks/index.js @@ -24,7 +24,8 @@ export { default as ColorPalette } from './color-palette'; export { default as Editable } from './editable'; export { default as EditableProvider } from './editable/provider'; export { default as InspectorControls } from './inspector-controls'; -export { default as MediaUploadButton } from './media-upload-button'; +export { default as MediaUpload } from './media-upload'; +export { default as MediaUploadButton } from './media-upload/button'; export { default as TermTreeSelect } from './term-tree-select'; export { default as UrlInput } from './url-input'; export { default as UrlInputButton } from './url-input/button'; diff --git a/blocks/library/audio/index.js b/blocks/library/audio/index.js index 99d0251427e4c1..8858e07c414ddb 100644 --- a/blocks/library/audio/index.js +++ b/blocks/library/audio/index.js @@ -15,7 +15,7 @@ import { Component } from '@wordpress/element'; import './style.scss'; import './editor.scss'; import { registerBlockType } from '../../api'; -import MediaUploadButton from '../../media-upload-button'; +import MediaUpload from '../../media-upload'; import Editable from '../../editable'; import BlockControls from '../../block-controls'; import BlockAlignmentToolbar from '../../block-alignment-toolbar'; @@ -118,7 +118,7 @@ registerBlockType( 'core/audio', { key="placeholder" icon="media-audio" label={ __( 'Audio' ) } - instructions={ __( 'Select an audio file from your library, or upload a new one:' ) } + instructions={ __( 'Select an audio file from your library, or upload a new one' ) } className={ className }>
- - { __( 'Add from Media Library' ) } - + render={ ( { open } ) => ( + + ) } + /> , ]; } diff --git a/blocks/library/audio/test/__snapshots__/index.js.snap b/blocks/library/audio/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..604ccd6fbaf998 --- /dev/null +++ b/blocks/library/audio/test/__snapshots__/index.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`core/audio block edit matches snapshot 1`] = ` +
+
+ + Audio +
+
+ Select an audio file from your library, or upload a new one +
+
+
+ + +
+ *** Mock(Media upload button) *** +
+
+`; diff --git a/blocks/library/audio/test/index.js b/blocks/library/audio/test/index.js new file mode 100644 index 00000000000000..25a087195f5056 --- /dev/null +++ b/blocks/library/audio/test/index.js @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import '../'; +import { blockEditRender } from 'blocks/test/helpers'; + +jest.mock( 'blocks/media-upload', () => () => '*** Mock(Media upload button) ***' ); + +describe( 'core/audio', () => { + test( 'block edit matches snapshot', () => { + const wrapper = blockEditRender( 'core/audio' ); + + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/blocks/library/button/test/__snapshots__/index.js.snap b/blocks/library/button/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..60a4909e58e170 --- /dev/null +++ b/blocks/library/button/test/__snapshots__/index.js.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`core/button block edit matches snapshot 1`] = ` + +
+ + + Add text… + +
+
+`; diff --git a/blocks/library/button/test/index.js b/blocks/library/button/test/index.js new file mode 100644 index 00000000000000..8e4fdf4979f539 --- /dev/null +++ b/blocks/library/button/test/index.js @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import '../'; +import { blockEditRender } from 'blocks/test/helpers'; + +describe( 'core/button', () => { + test( 'block edit matches snapshot', () => { + const wrapper = blockEditRender( 'core/button' ); + + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/blocks/library/code/test/__snapshots__/index.js.snap b/blocks/library/code/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..ff100192b8158c --- /dev/null +++ b/blocks/library/code/test/__snapshots__/index.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`core/code block edit matches snapshot 1`] = ` +
+