├── .nvmrc ├── assets ├── banner-772x250.png ├── icon-128x128.png ├── icon-256x256.png ├── banner-1544x500.png ├── icon.svg └── src │ ├── style.scss │ ├── settings │ └── index.js │ ├── index.js │ ├── content-converter │ └── index.js │ ├── conversion │ └── index.js │ ├── restore │ └── index.js │ └── utilities │ └── index.js ├── .prettierrc ├── webpack.config.js ├── phpunit.xml.dist ├── .gitignore ├── .editorconfig ├── lib ├── content-patcher │ ├── patchers │ │ ├── class-preconversionpatcherabstract.php │ │ ├── interface-patcher.php │ │ ├── interface-preconversionpatcher.php │ │ ├── class-patcherabstract.php │ │ ├── class-wp-filters-patcher.php │ │ ├── class-block-decode-patcher.php │ │ ├── class-block-encode-patcher.php │ │ ├── class-shortcodepreconversionpatcher.php │ │ ├── class-paragraphpatcher.php │ │ ├── class-blockquotepatcher.php │ │ ├── class-shortcodemodulepatcher.php │ │ ├── class-shortcodepullquotepatcher.php │ │ ├── class-audiopatcher.php │ │ ├── class-videopatcher.php │ │ └── class-captionimgpatcher.php │ ├── interface-patch-handler.php │ ├── class-patchhandler.php │ └── elementManipulators │ │ ├── class-wpblockmanipulator.php │ │ ├── class-squarebracketselementmanipulator.php │ │ └── class-htmlelementmanipulator.php └── class-converter.php ├── .github ├── workflows │ └── auto-merge.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── existing-content-conversion-bug.md │ └── new-content-conversion-request.md ├── newspack-content-converter.php ├── phpcs.xml ├── composer.json ├── tests ├── bootstrap.php ├── fixtures │ └── unit │ │ └── content-patcher │ │ └── patchers │ │ ├── class-dataprovidershortcodepreconversionpatcher.php │ │ ├── class-dataprovidershortcodepullquotepatcher.php │ │ ├── class-dataprovidershortcodemodulepatcher.php │ │ ├── class-dataproviderparagraphpatcher.php │ │ └── class-dataproviderblockquotepatcher.php └── unit │ └── content-patcher │ └── patchers │ ├── test-shortcode-preconversion-patcher.php │ ├── test-shortcodepullquote-patcher.php │ ├── test-shortcodemodule-patcher.php │ ├── test-paragraph-patcher.php │ ├── test-blockquote-patcher.php │ ├── test-video-patcher.php │ ├── test-audio-patcher.php │ └── test-caption-img-patcher.php ├── package.json ├── README.md └── bin └── install-wp-tests.sh /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/newspack-content-converter/HEAD/assets/banner-772x250.png -------------------------------------------------------------------------------- /assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/newspack-content-converter/HEAD/assets/icon-128x128.png -------------------------------------------------------------------------------- /assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/newspack-content-converter/HEAD/assets/icon-256x256.png -------------------------------------------------------------------------------- /assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/newspack-content-converter/HEAD/assets/banner-1544x500.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | useTabs: true 2 | tabWidth: 2 3 | printWidth: 100 4 | singleQuote: true 5 | trailingComma: es5 6 | bracketSpacing: true 7 | parenSpacing: true 8 | jsxBracketSameLine: false 9 | semi: true 10 | arrowParens: avoid 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | **** WARNING: No ES6 modules here. Not transpiled! **** 3 | */ 4 | /* eslint-disable import/no-nodejs-modules */ 5 | 6 | const getBaseWebpackConfig = require( 'newspack-scripts/config/getWebpackConfig' ); 7 | 8 | const webpackConfig = getBaseWebpackConfig( 9 | { 10 | entry: './assets/src/', 11 | } 12 | ); 13 | 14 | module.exports = webpackConfig; 15 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests/unit/content-patcher/patchers/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /assets/dist 4 | /assets/release 5 | 6 | # dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /build 16 | /.cache 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | .idea 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | convert.log 31 | 32 | /vendor/ 33 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [{.jshintrc,*.json,*.yml}] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [{*.txt,wp-config-sample.php}] 22 | end_of_line = crlf 23 | -------------------------------------------------------------------------------- /lib/content-patcher/patchers/class-preconversionpatcherabstract.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | . 25 | 26 | */vendor/* 27 | 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automattic/newspack-content-converter", 3 | "description": "Mass conversion of pre-Gutenberg Classic HTML post content to the Gutenberg blocks content.", 4 | "license": "GPL-2.0-or-later", 5 | "autoload": { 6 | "classmap": [ 7 | "lib/", 8 | "tests/" 9 | ] 10 | }, 11 | "require": { 12 | "composer/installers": "^2.0" 13 | }, 14 | "require-dev": { 15 | "automattic/vipwpcs": "^3.0.0", 16 | "wp-coding-standards/wpcs": "^3.0", 17 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", 18 | "phpcompatibility/phpcompatibility-wp": "^2.1", 19 | "yoast/phpunit-polyfills": "^1.0", 20 | "phpunit/phpunit": "^9.5" 21 | }, 22 | "config": { 23 | "allow-plugins": { 24 | "composer/installers": true, 25 | "dealerdirect/phpcodesniffer-composer-installer": true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/content-patcher/patchers/interface-patcher.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/unit/content-patcher/patchers/class-dataprovidershortcodepreconversionpatcher.php: -------------------------------------------------------------------------------- 1 | patcher = new ShortcodePreconversionPatcher(); 39 | $this->data_provider = new DataProviderShortcodePreconversionPatcher(); 40 | } 41 | 42 | /** 43 | * If a gallery shortcode is not starting on a new line, break it in to a new line. 44 | */ 45 | public function test_prepend_gallery_shortcodes_with_new_line() { 46 | $html_before_patching = $this->data_provider->get_html_with_gallery_shortcodes_mixed(); 47 | $expected = $this->data_provider->get_html_with_gallery_shortcodes_mixed_expected(); 48 | 49 | $post_id = 1; 50 | $actual = $this->patcher->patch_html_source( $html_before_patching, $post_id ); 51 | $this->assertSame( $expected, $actual ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /assets/src/style.scss: -------------------------------------------------------------------------------- 1 | @import '~newspack-components/shared/scss/colors'; 2 | 3 | #wpwrap { 4 | background: white; 5 | 6 | #wpcontent { 7 | padding: 0; 8 | } 9 | } 10 | 11 | .components-card { 12 | margin: 2rem auto; 13 | max-width: 520px; 14 | 15 | &__header { 16 | h2 { 17 | margin-bottom: 0; 18 | } 19 | 20 | p { 21 | margin-top: 0; 22 | } 23 | } 24 | 25 | .components-notice { 26 | margin: 1em 0; 27 | } 28 | } 29 | 30 | .newspack-header { 31 | align-items: center; 32 | border-bottom: 1px solid #ddd; 33 | display: flex; 34 | flex-wrap: wrap; 35 | height: 59px; 36 | justify-content: space-between; 37 | margin-bottom: 2.5rem; 38 | padding: 0 24px 0 74px; 39 | 40 | h2, p { 41 | margin: 0; 42 | } 43 | } 44 | 45 | .newspack-icon { 46 | background: $primary-600; 47 | left: 0; 48 | padding: 14px; 49 | position: absolute; 50 | top: 46px; 51 | z-index: 3; 52 | 53 | @media screen and (min-width: 601px) { 54 | top: 0; 55 | } 56 | } 57 | 58 | .newspack-content-converter { 59 | &__wrapper { 60 | background: white; 61 | bottom: 0; 62 | display: block; 63 | flex-direction: column; 64 | left: 0; 65 | overflow-y: auto; 66 | position: fixed; 67 | right: 0; 68 | top: 0; 69 | z-index: 9999; 70 | 71 | &.is-active { 72 | cursor: wait; 73 | } 74 | 75 | .components-spinner { 76 | margin: 1em 0; 77 | } 78 | 79 | .components-button + .components-button { 80 | margin-left: 8px; 81 | } 82 | 83 | .newspack-logo { 84 | margin: 0 20px; 85 | } 86 | } 87 | 88 | &__batch { 89 | .components-spinner { 90 | margin: 0 1em 0 0; 91 | } 92 | } 93 | } 94 | 95 | .interface-interface-skeleton { 96 | &__body { 97 | opacity: 0; 98 | } 99 | 100 | &__footer { 101 | display: none; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /assets/src/settings/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component, Fragment } from '@wordpress/element'; 5 | import { __ } from '@wordpress/i18n'; 6 | import { 7 | Button, 8 | Card, 9 | CardBody, 10 | CardFooter, 11 | TextControl 12 | } from '@wordpress/components'; 13 | 14 | /** 15 | * Newspack dependencies. 16 | */ 17 | import { NewspackIcon } from 'newspack-components'; 18 | 19 | /** 20 | * Internal dependencies. 21 | */ 22 | import { fetchSettingsInfo, runMultiplePosts } from '../utilities'; 23 | 24 | class Settings extends Component { 25 | /** 26 | * Constructor. 27 | */ 28 | constructor( props ) { 29 | super( props ); 30 | 31 | this.state = { 32 | conversionContentTypesCsv: '...', 33 | conversionContentStatusesCsv: '...', 34 | }; 35 | } 36 | 37 | componentDidMount() { 38 | return fetchSettingsInfo().then( response => { 39 | if ( response ) { 40 | const { conversionContentTypesCsv, conversionContentStatusesCsv } = response; 41 | this.setState( { 42 | conversionContentTypesCsv, 43 | conversionContentStatusesCsv, 44 | } ); 45 | } 46 | return new Promise( ( resolve, reject ) => resolve() ); 47 | } ); 48 | } 49 | 50 | /* 51 | * render(). 52 | */ 53 | render() { 54 | const { conversionContentTypesCsv, conversionContentStatusesCsv } = this.state; 55 | 56 | return ( 57 | 58 |
59 | 60 |

{ __( 'Content Converter / Settings' ) }

61 |
62 | 63 | 64 |

65 | { __( 66 | 'The type of HTML content to be converted to Gutenberg blocks is specified here.' 67 | ) } 68 |

69 | 74 | 79 |
80 | 81 | 87 | 88 |
89 |
90 | ); 91 | } 92 | } 93 | 94 | export default Settings; 95 | -------------------------------------------------------------------------------- /lib/content-patcher/patchers/class-block-decode-patcher.php: -------------------------------------------------------------------------------- 1 | decode_post_content( $block_content ); 26 | } 27 | 28 | /** 29 | * Decode blocks in string from base64. 30 | * 31 | * @param string $html_content String to decode. 32 | * 33 | * @return string The string with all blocks decoded. 34 | */ 35 | private function decode_post_content( string $html_content ): string { 36 | if ( ! str_contains( $html_content, BlockEncodePatcher::ENCODED_ANCHOR ) ) { 37 | return $html_content; 38 | } 39 | $blocks = parse_blocks( $html_content ); 40 | $encoded_blocks = array_filter( $blocks, fn( $block ) => str_contains( $block['innerHTML'], BlockEncodePatcher::ENCODED_ANCHOR ) ); 41 | 42 | if ( empty( $encoded_blocks ) ) { 43 | return $html_content; 44 | } 45 | foreach ( $encoded_blocks as $idx => $encoded ) { 46 | $decoded = $this->decode_block( $encoded['innerHTML'] ); 47 | if ( ! empty( $decoded ) ) { 48 | $blocks[ $idx ] = $decoded; 49 | } 50 | } 51 | 52 | return serialize_blocks( $blocks ); 53 | } 54 | 55 | /** 56 | * Decode a block from base64. 57 | * 58 | * @param string $encoded_block Block to decode. 59 | * 60 | * @return array The decoded block. 61 | */ 62 | private function decode_block( string $encoded_block ): array { 63 | $pattern = '/\\' . BlockEncodePatcher::ENCODED_ANCHOR . '([A-Za-z0-9+\\/=]+)\]/'; 64 | // See https://base64.guru/learn/base64-characters for chars in base64. 65 | preg_match( $pattern, $encoded_block, $matches ); 66 | if ( empty( $matches[1] ) ) { 67 | return []; 68 | } 69 | 70 | $parsed = parse_blocks( base64_decode( $matches[1], true ) ); 71 | if ( ! empty( $parsed[0]['blockName'] ) ) { 72 | return $parsed[0]; 73 | } 74 | 75 | return []; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/content-patcher/patchers/class-block-encode-patcher.php: -------------------------------------------------------------------------------- 1 | encode_post_content( $html_content ); 27 | } 28 | 29 | /** 30 | * Encode Gutenberg blocks in given string as base64. 31 | * 32 | * @param string $html The string content to encode. 33 | * 34 | * @return string The string with all blocks base64 encoded. 35 | */ 36 | private function encode_post_content( $html ) { 37 | if ( ! str_contains( $html, ' 25 | [pullquote author="Mary Filardo, president of the 21st Century School Fund" description="" style="new-pullquote"]“It’s just one more nail in the coffin of small towns that are already struggling. The county hospital closed, and the mom-and-pop shops are gone because Walmart opened. When you lose the schools, you lose the community.” [/pullquote] 26 | 27 | CONTENT; 28 | } 29 | 30 | /** 31 | * Get a patched pullquote shortcode. 32 | * 33 | * @return string Patched block content. 34 | */ 35 | public static function get_patched_block_expected() { 36 | return << 38 |

“It’s just one more nail in the coffin of small towns that are already struggling. The county hospital closed, and the mom-and-pop shops are gone because Walmart opened. When you lose the schools, you lose the community.”

Mary Filardo, president of the 21st Century School Fund
39 | 40 | CONTENT; 41 | } 42 | 43 | /** 44 | * Get an unpatched pullquote shortcode with no defined author. 45 | * 46 | * @return string Unpatched block content. 47 | */ 48 | public static function get_unpatched_block_no_author() { 49 | return << 51 | [pullquote author="" description="" style="new-pullquote"]The nation’s school districts spend about $46 billion less per year on facility upkeep than is needed to maintain “healthy and safe” learning environments, according to the 21st Century School Fund.[/pullquote] 52 | 53 | CONTENT; 54 | } 55 | 56 | /** 57 | * Get a patched pullquote shortcode with no defined author. 58 | * 59 | * @return string Patched block content. 60 | */ 61 | public static function get_patched_block_no_author_expected() { 62 | return << 64 |

The nation’s school districts spend about $46 billion less per year on facility upkeep than is needed to maintain “healthy and safe” learning environments, according to the 21st Century School Fund.

65 | 66 | CONTENT; 67 | } 68 | 69 | /** 70 | * Get an unpatched non-relevant content. 71 | * 72 | * @return string Unpatched block content. 73 | */ 74 | public static function get_unpatched_blocks_non_pertinent() { 75 | return << 77 |

This is a paragraph.

78 | 79 | 80 | 81 | [nonpertinent shortcode att="test"]This is some content[/nonpertinent] 82 | 83 | 84 | 85 |

This is a paragraph after.

86 | 87 | CONTENT; 88 | } 89 | 90 | /** 91 | * Get patched non-relevant content. 92 | * 93 | * @return string Patched block content. 94 | */ 95 | public static function get_patched_blocks_non_pertinent_expected() { 96 | return << 98 |

This is a paragraph.

99 | 100 | 101 | 102 | [nonpertinent shortcode att="test"]This is some content[/nonpertinent] 103 | 104 | 105 | 106 |

This is a paragraph after.

107 | 108 | CONTENT; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /assets/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { render } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import ContentConverter from './content-converter'; 10 | import Settings from './settings'; 11 | import Restore from './restore'; 12 | import Conversion from './conversion'; 13 | import './style.scss'; 14 | 15 | const nccGetElementByClassName = function( className ) { 16 | const elements = document.getElementsByClassName( className ); 17 | if ( 'undefined' == typeof elements || ! elements.length ) 18 | throw 'Not found element by class name ' + className; 19 | return elements[ 0 ]; 20 | }; 21 | 22 | const nccHideElementByClass = function( className ) { 23 | nccGetElementByClassName( className ).style.display = 'none'; 24 | }; 25 | 26 | const nccInsertRootAdjacentToElementByClass = function( className ) { 27 | nccGetElementByClassName( className ).insertAdjacentHTML( 'afterend', '
' ); 28 | }; 29 | 30 | const nccRenderRoot = function() { 31 | window.onbeforeunload = function() {}; 32 | render( 33 | , 34 | document.getElementById( 'root' ) 35 | ); 36 | }; 37 | 38 | // Wrapper function which enables retrying a callback after a timeout interval and for a defined maxAttempts (useful to retry 39 | // actions for elements which haven't yet been injected into DOM). 40 | function nccCallbackWithRetry( callback, callbackParam, maxAttempts = 10, timeout = 1000 ) { 41 | return new Promise( function( resolve, reject ) { 42 | const doCallback = function( attempt ) { 43 | try { 44 | callback( callbackParam ); 45 | resolve(); 46 | } catch ( e ) { 47 | if ( 0 == attempt ) { 48 | console.log( 'Final CSS warning: ' + e ); 49 | } else { 50 | setTimeout( function() { 51 | doCallback( attempt - 1 ); 52 | }, timeout ); 53 | console.log( e ); 54 | } 55 | } 56 | }; 57 | doCallback( maxAttempts ); 58 | } ); 59 | } 60 | 61 | // Check if the root div is loaded, and if not alerts the user. 62 | const nccCheckRootDivIsLoaded = function() { 63 | let retries = 0; 64 | let maxRetries = 15; 65 | let retryInterval = 1000; 66 | const interval = setInterval(() => { 67 | const root = document.getElementById("root"); 68 | if (root) { 69 | // Stop if found. 70 | clearInterval(interval); 71 | } else { 72 | retries++; 73 | if (retries > (maxRetries - 1)) { 74 | alert('It looks like something may be preventing the Newspack Content Converter from loading. Please try refreshing the page. If the problem persists, temporarily deactivate all other active plugins, and then try refreshing the page.'); 75 | // Stop. 76 | clearInterval(interval); 77 | } 78 | } 79 | }, retryInterval); 80 | } 81 | // If this is the conversion batch page (/wp-admin/post-new.php?newspack-content-converter), check if the root div is loaded. 82 | const isConversionPage = window.location.pathname === '/wp-admin/post-new.php' && window.location.search === '?newspack-content-converter'; 83 | if ( isConversionPage ) { 84 | nccCheckRootDivIsLoaded(); 85 | } 86 | 87 | window.onload = function() { 88 | const div_settings = document.getElementById( 'ncc-settings' ); 89 | const div_restore = document.getElementById( 'ncc-restore' ); 90 | const div_conversion = document.getElementById( 'ncc-conversion' ); 91 | 92 | if ( typeof div_settings != 'undefined' && div_settings != null ) { 93 | render( , div_settings ); 94 | } else if ( typeof div_restore != 'undefined' && div_restore != null ) { 95 | render( , div_restore ); 96 | } else if ( typeof div_conversion != 'undefined' && div_conversion != null ) { 97 | render( , div_conversion ); 98 | } else { 99 | // Converter app sits on top of the Gutenberg Block Editor. 100 | nccCallbackWithRetry( nccHideElementByClass, 'edit-post-header' ); 101 | nccCallbackWithRetry( nccHideElementByClass, 'edit-post-layout__content' ); 102 | nccCallbackWithRetry( nccHideElementByClass, 'edit-post-sidebar' ); 103 | nccCallbackWithRetry( nccInsertRootAdjacentToElementByClass, 'edit-post-header' ); 104 | nccCallbackWithRetry( nccRenderRoot ); 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /lib/content-patcher/patchers/class-shortcodemodulepatcher.php: -------------------------------------------------------------------------------- 1 | square_brackets_element_manipulator = new SquareBracketsElementManipulator(); 42 | $this->wp_block_manipulator = new WpBlockManipulator(); 43 | } 44 | 45 | /** 46 | * See the \NewspackContentConverter\ContentPatcher\Patchers\PatcherInterface::patch_blocks_contents for description. 47 | * 48 | * @param string $source_blocks Block content after conversion to blocks. 49 | * @param string $source_html HTML source, original content before conversion. 50 | * @param int $post_id Post ID. 51 | * 52 | * @return string|false 53 | */ 54 | public function patch_blocks_contents( $source_blocks, $source_html, $post_id ) { 55 | $matches_blocks = $this->wp_block_manipulator->match_wp_block( 'wp:shortcode', $source_blocks ); 56 | if ( ! $matches_blocks ) { 57 | return $source_blocks; 58 | } 59 | 60 | foreach ( $matches_blocks[0] as $matched_block ) { 61 | $block = $matched_block[0]; 62 | if ( false === strpos( $block, '[/module]' ) ) { 63 | continue; 64 | } 65 | 66 | $converted_block = $this->convert_shortcode_block_to_pullquote( $block ); 67 | $source_blocks = str_replace( $block, $converted_block, $source_blocks ); 68 | } 69 | 70 | return $source_blocks; 71 | } 72 | 73 | /** 74 | * Convert a shortcode block with the Lorgo theme's module shortcode into a pullquote block. 75 | * 76 | * @param string $block Raw block content. 77 | * @return string New block content. 78 | */ 79 | protected function convert_shortcode_block_to_pullquote( $block ) { 80 | // Strip any fancy quotes that may be breaking shortcode attributes. 81 | // @see https://github.com/Automattic/newspack-content-converter/issues/11. 82 | $block = str_replace( '”', '"', $block ); 83 | 84 | // Remove newlines because they confuse the matchers. 85 | $block = str_replace( "\n", '', $block ); 86 | 87 | $shortcode_matches = $this->square_brackets_element_manipulator->match_elements_with_closing_tags( 'module', $block ); 88 | $shortcode = $shortcode_matches[0][0][0]; 89 | 90 | $alignment = $this->square_brackets_element_manipulator->get_attribute_value( 'align', $shortcode ); 91 | $alignment = ( $alignment && ( 'left' === $alignment || 'right' === $alignment ) ) ? $alignment : ''; 92 | 93 | // Get content. 94 | $allowed_tags = [ 95 | 'a' => [ 96 | 'href' => [], 97 | ], 98 | ]; 99 | $content = $this->square_brackets_element_manipulator->get_inner_text( 'module', $shortcode ); 100 | $content = trim( wp_kses( $content, $allowed_tags ) ); 101 | 102 | $alignment_object = $alignment ? '{"align":"' . $alignment . '"} ' : ''; 103 | $class = 'wp-block-pullquote'; 104 | if ( $alignment ) { 105 | $class .= ' align' . $alignment; 106 | } 107 | return '\n

$content

\n"; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/content-patcher/patchers/class-shortcodepullquotepatcher.php: -------------------------------------------------------------------------------- 1 | square_brackets_element_manipulator = new SquareBracketsElementManipulator(); 41 | $this->wp_block_manipulator = new WpBlockManipulator(); 42 | } 43 | 44 | /** 45 | * See the \NewspackContentConverter\ContentPatcher\Patchers\PatcherInterface::patch_blocks_contents for description. 46 | * 47 | * @param string $source_blocks Block content after conversion to blocks. 48 | * @param string $source_html HTML source, original content before conversion. 49 | * @param int $post_id Post ID. 50 | * 51 | * @return string|false 52 | */ 53 | public function patch_blocks_contents( $source_blocks, $source_html, $post_id ) { 54 | $matches_blocks = $this->wp_block_manipulator->match_wp_block( 'wp:shortcode', $source_blocks ); 55 | if ( ! $matches_blocks ) { 56 | return $source_blocks; 57 | } 58 | 59 | foreach ( $matches_blocks[0] as $matched_block ) { 60 | $block = $matched_block[0]; 61 | if ( false === strpos( $block, '[/pullquote]' ) ) { 62 | continue; 63 | } 64 | 65 | $converted_block = $this->convert_shortcode_block_to_pullquote( $block ); 66 | $source_blocks = str_replace( $block, $converted_block, $source_blocks ); 67 | } 68 | 69 | return $source_blocks; 70 | } 71 | 72 | /** 73 | * Convert a shortcode block with the pullquote shortcode into a pullquote block. 74 | * 75 | * The pullquote's 'author' attribute gets converted to a element, e.g. 76 | * 77 | * [pullquote author"Arthur Author"]pulled text[\pullquote] 78 | * 79 | * gets converted to 80 | * 81 | *
82 | *
83 | *

pulled text

84 | * Arthur Author 85 | *
86 | *
87 | * 88 | * 89 | * @param string $block Raw block content. 90 | * @return string New block content. 91 | */ 92 | protected function convert_shortcode_block_to_pullquote( $block ) { 93 | // Remove newlines because they confuse the matchers. 94 | $block = str_replace( "\n", '', $block ); 95 | 96 | $shortcode_matches = $this->square_brackets_element_manipulator->match_elements_with_closing_tags( 'pullquote', $block ); 97 | $shortcode = $shortcode_matches[0][0][0]; 98 | 99 | // Get content. 100 | $allowed_tags = [ 101 | 'a' => [ 102 | 'href' => [], 103 | ], 104 | ]; 105 | $content = $this->square_brackets_element_manipulator->get_inner_text( 'pullquote', $shortcode ); 106 | $content = trim( wp_kses( $content, $allowed_tags ) ); 107 | 108 | // Get citation. 109 | $cite = ''; 110 | $author = trim( $this->square_brackets_element_manipulator->get_attribute_value( 'author', $shortcode ) ); 111 | if ( $author ) { 112 | $cite = '' . trim( wp_kses( $author, $allowed_tags ) ) . ''; 113 | } 114 | 115 | return "\n

$content

$cite
\n"; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/unit/content-patcher/patchers/test-paragraph-patcher.php: -------------------------------------------------------------------------------- 1 | patcher = new ParagraphPatcher(); 39 | $this->data_provider = new DataProviderParagraphPatcher(); 40 | } 41 | 42 | /** 43 | * The patcher shoud patch the lost dir attribute. 44 | */ 45 | public function test_should_patch_dir_attribute() { 46 | $html = '

AAA

'; 47 | $blocks_before_patching = '

AAA

'; 48 | $expected = '

AAA

'; 49 | 50 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 51 | 52 | $this->assertSame( $expected, $actual ); 53 | 54 | } 55 | 56 | /** 57 | * Tests that blocks code will not get modified if running into an inconsistency between HTML and non-patched blocks code. 58 | */ 59 | public function test_should_not_modify_source_if_html_is_html_code_inconsistent_with_blocks_code() { 60 | $html = $this->data_provider->get_inconsistent_sources_html(); 61 | $blocks_before_patching = $this->data_provider->get_inconsistent_sources_before_patching(); 62 | $expected = $this->data_provider->get_inconsistent_sources_blocks_patched_expected(); 63 | 64 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 65 | 66 | $this->assertSame( $expected, $actual ); 67 | } 68 | 69 | /** 70 | * Tests that blocks code will not get modified if HTML code is not supposed to be patched by this patcher. 71 | */ 72 | public function test_should_not_modify_source_if_html_not_pertinent_to_this_patcher() { 73 | $html = $this->data_provider->get_html_is_non_pertinent_html(); 74 | $blocks_before_patching = $this->data_provider->get_html_is_non_pertinent_before_patching(); 75 | $expected = $this->data_provider->get_html_is_non_pertinent_blocks_patched_expected(); 76 | 77 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 78 | 79 | $this->assertSame( $expected, $actual ); 80 | } 81 | 82 | /** 83 | * Should patch multiple paragraphs. 84 | */ 85 | public function test_should_patch_dir_attribute_on_multiple_paragraph_elements() { 86 | $html = $this->data_provider->get_multiple_paragraphs_html(); 87 | $blocks_before_patching = $this->data_provider->get_multiple_paragraphs_blocks_before_patching(); 88 | $expected = $this->data_provider->get_multiple_paragraphs_blocks_patched_expected(); 89 | 90 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 91 | 92 | $this->assertSame( $expected, $actual ); 93 | } 94 | 95 | /** 96 | * This is a case where not all paragraphs have the dir attribute. Patcher should not patch those that don't. 97 | */ 98 | public function test_should_skip_patching_paragraphs_which_dont_have_the_dir_attribute() { 99 | $html = $this->data_provider->get_some_skipped_paragraphs_html(); 100 | $blocks_before_patching = $this->data_provider->get_some_skipped_paragraphs_blocks_before_patching(); 101 | $expected = $this->data_provider->get_some_skipped_paragraphs_blocks_patched_expected(); 102 | 103 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 104 | 105 | $this->assertSame( $expected, $actual ); 106 | } 107 | 108 | /** 109 | * This test will become important when Gutenberg fixes conversion of this particular case, so we need to make sure 110 | * we won't patch it twice. 111 | */ 112 | public function test_should_skip_patching_paragraphs_that_already_have_the_dir_attribute() { 113 | $html = $this->data_provider->get_some_paragraphs_ok_html(); 114 | $blocks_before_patching = $this->data_provider->get_some_paragraphs_ok_blocks_before_patching(); 115 | $expected = $this->data_provider->get_some_paragraphs_ok_blocks_patched_expected(); 116 | 117 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 118 | 119 | $this->assertSame( $expected, $actual ); 120 | } 121 | 122 | /** 123 | * Runs a comprehensive example and checks for validity. 124 | */ 125 | public function test_should_correctly_patch_a_comprehensive_paragraph_conversion_example() { 126 | $html = $this->data_provider->get_comprehensive_html(); 127 | $blocks_before_patching = $this->data_provider->get_comprehensive_blocks_before_patching(); 128 | $expected = $this->data_provider->get_comprehensive_blocks_patched_expected(); 129 | 130 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 131 | 132 | $this->assertSame( $expected, $actual ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/unit/content-patcher/patchers/test-blockquote-patcher.php: -------------------------------------------------------------------------------- 1 | fixtures_dir = dirname( __FILE__ ) . '/../../../fixtures/unit/content-patcher/patchers/'; 38 | 39 | require_once $this->fixtures_dir . 'class-dataproviderblockquotepatcher.php'; 40 | 41 | $this->patcher = new BlockquotePatcher(); 42 | $this->data_provider = new DataProviderBlockquotePatcher(); 43 | } 44 | 45 | /** 46 | * The patcher shloud patch the lost data-lang attribute. 47 | */ 48 | public function test_should_patch_datalang_attribute() { 49 | $html = '
AAA
'; 50 | $blocks_before_patching = '
AAA
'; 51 | $expected = '
AAA
'; 52 | 53 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 54 | 55 | $this->assertSame( $expected, $actual ); 56 | 57 | } 58 | 59 | /** 60 | * Tests that blocks code will not get modified if running into an inconsistency between HTML and non-patched blocks code. 61 | */ 62 | public function test_should_not_modify_source_if_html_is_html_code_inconsistent_with_blocks_code() { 63 | $html = $this->data_provider->get_inconsistent_sources_html(); 64 | $blocks_before_patching = $this->data_provider->get_inconsistent_sources_before_patching(); 65 | $expected = $this->data_provider->get_inconsistent_sources_blocks_patched_expected(); 66 | 67 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 68 | 69 | $this->assertSame( $expected, $actual ); 70 | } 71 | 72 | /** 73 | * Tests that blocks code will not get modified if HTML code is not supposed to be patched by this patcher. 74 | */ 75 | public function test_should_not_modify_source_if_html_not_pertinent_to_this_patcher() { 76 | $html = $this->data_provider->get_html_is_non_pertinent_html(); 77 | $blocks_before_patching = $this->data_provider->get_html_is_non_pertinent_before_patching(); 78 | $expected = $this->data_provider->get_html_is_non_pertinent_blocks_patched_expected(); 79 | 80 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 81 | 82 | $this->assertSame( $expected, $actual ); 83 | } 84 | 85 | /** 86 | * Should patch multiple blockquotes. 87 | */ 88 | public function test_should_patch_datalang_attribute_on_multiple_paragraph_elements() { 89 | $html = $this->data_provider->get_multiple_blockquotes_html(); 90 | $blocks_before_patching = $this->data_provider->get_multiple_blockquotes_blocks_before_patching(); 91 | $expected = $this->data_provider->get_multiple_blockquotes_blocks_patched_expected(); 92 | 93 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 94 | 95 | $this->assertSame( $expected, $actual ); 96 | } 97 | 98 | /** 99 | * This is a case where not all blockquotes have the data-lang attribute. Patcher should not patch those that don't. 100 | */ 101 | public function test_should_skip_patching_blockquotess_which_dont_have_the_datalang_attribute() { 102 | $html = $this->data_provider->get_some_skipped_blockquotes_html(); 103 | $blocks_before_patching = $this->data_provider->get_some_skipped_blockquotes_blocks_before_patching(); 104 | $expected = $this->data_provider->get_some_skipped_blockquotes_blocks_patched_expected(); 105 | 106 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 107 | 108 | $this->assertSame( $expected, $actual ); 109 | } 110 | 111 | /** 112 | * This test will become important when Gutenberg fixes conversion of this particular case, so we need to make sure 113 | * we won't patch it twice. 114 | */ 115 | public function test_should_skip_patching_blockquotes_that_already_have_the_datalang_attribute() { 116 | $html = $this->data_provider->get_some_blockquotes_ok_html(); 117 | $blocks_before_patching = $this->data_provider->get_some_blockquotes_ok_blocks_before_patching(); 118 | $expected = $this->data_provider->get_some_blockquotes_ok_blocks_patched_expected(); 119 | 120 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 121 | 122 | $this->assertSame( $expected, $actual ); 123 | } 124 | 125 | /** 126 | * Runs a comprehensive example and checks for validity. 127 | */ 128 | public function test_should_correctly_patch_a_comprehensive_blockquote_conversion_example() { 129 | $html = $this->data_provider->get_comprehensive_html(); 130 | $blocks_before_patching = $this->data_provider->get_comprehensive_blocks_before_patching(); 131 | $expected = $this->data_provider->get_comprehensive_blocks_patched_expected(); 132 | 133 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 134 | 135 | $this->assertSame( $expected, $actual ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/fixtures/unit/content-patcher/patchers/class-dataprovidershortcodemodulepatcher.php: -------------------------------------------------------------------------------- 1 | 25 | [module align="left" width="half" type="pull-quote"]"The Board will add this to the ongoing investigation." —New Beginnings attorney Michelle Craig[/module] 26 | 27 | CONTENT; 28 | } 29 | 30 | /** 31 | * Get an patched left-aligned module shortcode. 32 | * 33 | * @return string Patched block content. 34 | */ 35 | public static function get_patched_block_left_expected() { 36 | return << 38 |

"The Board will add this to the ongoing investigation." —New Beginnings attorney Michelle Craig

39 | 40 | CONTENT; 41 | } 42 | 43 | /** 44 | * Get an unpatched right-aligned module shortcode. 45 | * 46 | * @return string Unpatched block content. 47 | */ 48 | public static function get_unpatched_block_right() { 49 | return << 51 | [module align="right" width=”full” type=”aside”]”The Board will add this to the ongoing investigation.” —New Beginnings attorney Michelle Craig[/module] 52 | 53 | CONTENT; 54 | } 55 | 56 | /** 57 | * Get an patched right-aligned module shortcode. 58 | * 59 | * @return string Patched block content. 60 | */ 61 | public static function get_patched_block_right_expected() { 62 | return << 64 |

"The Board will add this to the ongoing investigation." —New Beginnings attorney Michelle Craig

65 | 66 | CONTENT; 67 | } 68 | 69 | /** 70 | * Get unpatched center-aligned module shortcodes. 71 | * 72 | * @return string Unpatched block content. 73 | */ 74 | public static function get_unpatched_blocks_center() { 75 | return << 77 | [module align="center" width="half" type="pull-quote"]Test content[/module] 78 | 79 | 80 | 81 | [module]Test content[/module] 82 | 83 | CONTENT; 84 | } 85 | 86 | /** 87 | * Get an patched center-aligned module shortcode. 88 | * 89 | * @return string Patched block content. 90 | */ 91 | public static function get_patched_blocks_center_expected() { 92 | return << 94 |

Test content

95 | 96 | 97 | 98 |

Test content

99 | 100 | CONTENT; 101 | } 102 | 103 | /** 104 | * Get an unpatched module shortcode with unsupported HTML content. 105 | * 106 | * @return string Unpatched block content. 107 | */ 108 | public static function get_unpatched_block_unsupported_tags() { 109 | return << 111 | [module align=”right” width=”half” type=”aside”] 112 |
Previous coverage: F to D grade changes at Kennedy High School are suspicious, former administrator says
113 | [/module] 114 | 115 | CONTENT; 116 | } 117 | 118 | /** 119 | * Get an patched module shortcode with unsupported HTML content removed. 120 | * 121 | * @return string Patched block content. 122 | */ 123 | public static function get_patched_block_unsupported_tags_expected() { 124 | return << 126 |

Previous coverage: F to D grade changes at Kennedy High School are suspicious, former administrator says

127 | 128 | CONTENT; 129 | } 130 | 131 | /** 132 | * Get an unpatched non-relevant content. 133 | * 134 | * @return string Unpatched block content. 135 | */ 136 | public static function get_unpatched_blocks_non_pertinent() { 137 | return << 139 |

This is a paragraph.

140 | 141 | 142 | 143 | [nonpertinent shortcode att="test"]This is some content[/nonpertinent] 144 | 145 | 146 | 147 |

This is a paragraph after.

148 | 149 | CONTENT; 150 | } 151 | 152 | /** 153 | * Get patched non-relevant content. 154 | * 155 | * @return string Patched block content. 156 | */ 157 | public static function get_patched_blocks_non_pertinent_expected() { 158 | return << 160 |

This is a paragraph.

161 | 162 | 163 | 164 | [nonpertinent shortcode att="test"]This is some content[/nonpertinent] 165 | 166 | 167 | 168 |

This is a paragraph after.

169 | 170 | CONTENT; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 29 | WP_BRANCH=${WP_VERSION%\-*} 30 | WP_TESTS_TAG="branches/$WP_BRANCH" 31 | 32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 33 | WP_TESTS_TAG="branches/$WP_VERSION" 34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 37 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 38 | else 39 | WP_TESTS_TAG="tags/$WP_VERSION" 40 | fi 41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 42 | WP_TESTS_TAG="trunk" 43 | else 44 | # http serves a single offer, whereas https serves multiple. we only want one 45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 48 | if [[ -z "$LATEST_VERSION" ]]; then 49 | echo "Latest WordPress version could not be found" 50 | exit 1 51 | fi 52 | WP_TESTS_TAG="tags/$LATEST_VERSION" 53 | fi 54 | set -ex 55 | 56 | install_wp() { 57 | 58 | if [ -d $WP_CORE_DIR ]; then 59 | return; 60 | fi 61 | 62 | mkdir -p $WP_CORE_DIR 63 | 64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 65 | mkdir -p $TMPDIR/wordpress-nightly 66 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip 67 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ 68 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR 69 | else 70 | if [ $WP_VERSION == 'latest' ]; then 71 | local ARCHIVE_NAME='latest' 72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 73 | # https serves multiple offers, whereas http serves single. 74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 77 | LATEST_VERSION=${WP_VERSION%??} 78 | else 79 | # otherwise, scan the releases and get the most up to date minor version of the major release 80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 82 | fi 83 | if [[ -z "$LATEST_VERSION" ]]; then 84 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 85 | else 86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 87 | fi 88 | else 89 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 90 | fi 91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 93 | fi 94 | 95 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 96 | } 97 | 98 | install_test_suite() { 99 | # portable in-place argument for both GNU sed and Mac OSX sed 100 | if [[ $(uname -s) == 'Darwin' ]]; then 101 | local ioption='-i.bak' 102 | else 103 | local ioption='-i' 104 | fi 105 | 106 | # set up testing suite if it doesn't yet exist 107 | if [ ! -d $WP_TESTS_DIR ]; then 108 | # set up testing suite 109 | mkdir -p $WP_TESTS_DIR 110 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 111 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 112 | fi 113 | 114 | if [ ! -f wp-tests-config.php ]; then 115 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 116 | # remove all forward slashes in the end 117 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 118 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 119 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 120 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 123 | fi 124 | 125 | } 126 | 127 | install_db() { 128 | 129 | if [ ${SKIP_DB_CREATE} = "true" ]; then 130 | return 0 131 | fi 132 | 133 | # parse DB_HOST for port or socket references 134 | local PARTS=(${DB_HOST//\:/ }) 135 | local DB_HOSTNAME=${PARTS[0]}; 136 | local DB_SOCK_OR_PORT=${PARTS[1]}; 137 | local EXTRA="" 138 | 139 | if ! [ -z $DB_HOSTNAME ] ; then 140 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 141 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 142 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 143 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 144 | elif ! [ -z $DB_HOSTNAME ] ; then 145 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 146 | fi 147 | fi 148 | 149 | # create database 150 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 151 | } 152 | 153 | install_wp 154 | install_test_suite 155 | install_db 156 | -------------------------------------------------------------------------------- /assets/src/content-converter/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component, Fragment } from '@wordpress/element'; 5 | import { __ } from '@wordpress/i18n'; 6 | import { 7 | Button, 8 | Card, 9 | CardBody, 10 | CardFooter, 11 | Notice, 12 | Spinner 13 | } from '@wordpress/components'; 14 | 15 | /** 16 | * Newspack dependencies. 17 | */ 18 | import { NewspackIcon } from 'newspack-components'; 19 | 20 | /** 21 | * Internal dependencies. 22 | */ 23 | import { 24 | runMultiplePosts, 25 | fetchConversionBatch, 26 | } from '../utilities'; 27 | 28 | class ContentConverter extends Component { 29 | /** 30 | * Constructor. 31 | */ 32 | constructor( props ) { 33 | super( props ); 34 | 35 | this.state = { 36 | isActive: null, 37 | isConversionPrepared: null, 38 | isConversionFinished: null, 39 | ids: null, 40 | thisBatch: null, 41 | totalNumberOfBatches: null, 42 | }; 43 | } 44 | 45 | componentDidMount() { 46 | document.title = "Content Converter"; 47 | 48 | // Run a batch of conversions. 49 | return fetchConversionBatch() 50 | .then( response => { 51 | if ( response ) { 52 | const { isConversionPrepared, isConversionFinished, ids, thisBatch, totalNumberOfBatches } = response; 53 | // Starting conversion, setting isActive to true. 54 | this.setState( { 55 | isConversionPrepared, 56 | isConversionFinished, 57 | ids, 58 | thisBatch, 59 | totalNumberOfBatches, 60 | isActive: true, 61 | } ); 62 | // If conversion is not prepared and not finished, redirect to the plugin page. 63 | if ( '0' == isConversionPrepared && '0' == isConversionFinished ) { 64 | window.parent.location = '/wp-admin/admin.php?page=newspack-content-converter'; 65 | } else if ( ids ) { 66 | console.log( ' ----------------------- ABOUT TO CONVERT BATCH: ' + thisBatch + ' IDS: ' + ids ); 67 | return runMultiplePosts( ids ); 68 | } 69 | } 70 | 71 | return new Promise( ( resolve, reject ) => resolve() ); 72 | } ) 73 | .then( () => { 74 | return new Promise( ( resolve, reject ) => { 75 | console.log( ' ----------------------- FINISHED.' ); 76 | if ( this.state.ids && this.state.ids.length > 0 ) { 77 | // Conversion hasn't started yet, so isActive is null before it's either true or false. 78 | this.setState( { isActive: null } ); 79 | // This should disable the browser's "Reload page?" popup, although it doesn't always work as expected. 80 | window.onbeforeunload = function() {}; 81 | // Reload this window to pick up the next batch. 82 | window.location.reload( true ); 83 | } else { 84 | // No more posts to convert, so isActive is false. 85 | this.setState( { isActive: false } ); 86 | } 87 | 88 | return resolve(); 89 | } ); 90 | } ); 91 | } 92 | 93 | /* 94 | * render(). 95 | */ 96 | render() { 97 | const { isActive, thisBatch, totalNumberOfBatches } = this.state; 98 | 99 | if ( null == isActive ) { 100 | // This is the initial state of the interface, before conversion has started (true) or finished (false). 101 | return ( 102 |
103 |
104 | 105 |

{ __( 'Content Converter / Converting...' ) }

106 |
107 | 108 | 109 |

{ __( 'Conversion to Gutenberg blocks is in progress' ) }

110 |
111 | 112 | { __( 'Fetching posts for conversion... ' ) } 113 | 114 |
115 |
116 | ); 117 | } else if ( true == isActive ) { 118 | // Conversion is running. 119 | return ( 120 |
121 |
122 | 123 |

{ __( 'Content Converter / Converting...' ) }

124 |
125 | 126 | 127 |

{ __( 'Do not close this page!' ) }

128 |

{ __( 'Conversion to Gutenberg blocks is in progress' ) }

129 |

130 | { __( 131 | 'This page will occasionally automatically reload, and notify you when the conversion is complete.' 132 | ) } 133 |

134 | 135 | { __( 'If asked to Reload, chose yes.' ) } 136 | 137 |

138 | 139 | { __( 140 | 'To convert another batch in parallel and increase conversion speed (depending on your computer performance, no more than 10 max parallel browser tabs are usually recommended), ' 141 | ) } 142 | open an additional conversion tab. 143 | 144 |

145 |
146 | 147 | 148 |

{ __( 'Now processing batch' ) } { thisBatch }/{ totalNumberOfBatches }

149 |
150 |
151 |
152 | ); 153 | } else if ( false == isActive ) { 154 | // Conversion has finished. 155 | return ( 156 |
157 |
158 | 159 |

{ __( 'Content Converter / Conversion completed' ) }

160 |
161 | 162 | 163 | 164 | { __( 'All content has been converted.' ) } 165 | 166 | 167 | 168 | 171 | 172 | 173 |
174 | ); 175 | } 176 | } 177 | } 178 | 179 | export default ContentConverter; 180 | 181 | -------------------------------------------------------------------------------- /lib/content-patcher/elementManipulators/class-wpblockmanipulator.php: -------------------------------------------------------------------------------- 1 | # end of opening tag 26 | .*? # anything in the middle 27 | (\<\!-- # beginning of the closing tag 28 | \s # followed by a space 29 | / # one forward slash 30 | %1$s # element name/designation, should be substituted by using sprintf(), eg. sprintf( $this_pattern, \'wp:video\' ); 31 | \s # followed by a space 32 | --\>) # end of block 33 | # "s" modifier also needed here to match accross multi-lines 34 | |xims'; 35 | 36 | /** 37 | * Matches a self-closing block element -- which is one that does NOT have both an opening tag `` and a closing 38 | * tag ``, but rather has just one "self-closing tag", e.g. ``. 39 | */ 40 | const PATTERN_WP_BLOCK_ELEMENT_SELFCLOSING = '| 41 | \<\!-- # beginning of the block element 42 | \s # followed by a space 43 | %s # element name/designation, should be substituted by using sprintf() 44 | .*? # anything in the middle 45 | \/--\> # ends with a self-closing tag 46 | |xims'; 47 | 48 | /** 49 | * Searches and matches block elements in given source. 50 | * Runs the preg_match_all() with the PREG_OFFSET_CAPTURE option, and returns the $match. 51 | * 52 | * @param string $block_name Block name to search for (match). 53 | * @param string $subject Blocks content source in which to search for blocks. 54 | * 55 | * @return array|null| $matches from the preg_match_all() or null. 56 | */ 57 | public function match_wp_block( $block_name, $subject ) { 58 | 59 | $pattern = sprintf( self::PATTERN_WP_BLOCK_ELEMENT, $block_name ); 60 | 61 | $preg_match_all_result = preg_match_all( $pattern, $subject, $matches, PREG_OFFSET_CAPTURE ); 62 | return ( false === $preg_match_all_result || 0 === $preg_match_all_result ) ? null : $matches; 63 | } 64 | 65 | /** 66 | * Searches and matches blocks in given source. 67 | * 68 | * Uses preg_match_all() with the PREG_OFFSET_CAPTURE option, and returns its $match. 69 | * 70 | * @param string $block_name Block name/designation to search for. 71 | * @param string $subject The Block source in which to search for the block occurences. 72 | * 73 | * @return array|null The `$matches` array as set by preg_match_all() with the PREG_OFFSET_CAPTURE option, or null if no matches found. 74 | */ 75 | public function match_wp_block_selfclosing( $block_name, $subject ) { 76 | 77 | $pattern = sprintf( self::PATTERN_WP_BLOCK_ELEMENT_SELFCLOSING, $block_name ); 78 | $preg_match_all_result = preg_match_all( $pattern, $subject, $matches, PREG_OFFSET_CAPTURE ); 79 | 80 | return ( false === $preg_match_all_result || 0 === $preg_match_all_result ) ? null : $matches; 81 | } 82 | 83 | /** 84 | * Gets an attribute's value from the block element's header. 85 | * 86 | * @param string $block_element The block element, accepts a multiline string. 87 | * @param string $attribute_name Attribute name. 88 | * 89 | * @return string|null Attribute value. 90 | */ 91 | public function get_attribute( $block_element, $attribute_name ) { 92 | $block_element_lines = explode( "\n", $block_element ); 93 | $block_element_1st_line = $block_element_lines[0]; 94 | 95 | $curly_open_pos = strpos( $block_element_1st_line, '{' ); 96 | $curly_close_pos = strpos( $block_element_1st_line, '}' ); 97 | if ( false === $curly_open_pos || false === $curly_close_pos ) { 98 | return null; 99 | } 100 | 101 | $attributes_json = substr( $block_element_1st_line, $curly_open_pos, $curly_close_pos - $curly_open_pos + 1 ); 102 | $attributes = json_decode( $attributes_json, true ); 103 | 104 | return $attributes[ $attribute_name ] ?? null; 105 | } 106 | 107 | /** 108 | * Adds an attribute to the block element's header. 109 | * It doesn't check whethet the attribute already exists, simply appends it to the block definition. 110 | * 111 | * @param string $block_element The block element, accepts a multiline string. 112 | * @param string $attribute_name Attribute name. 113 | * @param string $attribute_value Attribute value. 114 | * 115 | * @return string Updated block element. 116 | */ 117 | public function add_attribute( $block_element, $attribute_name, $attribute_value ) { 118 | $block_element_lines = explode( "\n", $block_element ); 119 | $block_element_1st_line = $block_element_lines[0]; 120 | 121 | $pos_close_curly = strrpos( $block_element_1st_line, '}' ); 122 | if ( false !== $pos_close_curly ) { 123 | // If some attributes already exist, append to those. 124 | $block_element_1st_line_patched = substr_replace( $block_element_1st_line, ',"' . $attribute_name . '":"' . $attribute_value . '"}', $pos_close_curly, $length = 1 ); 125 | } else { 126 | // Otherwise, add the curly brackets first. 127 | $pos_close_comment = strrpos( $block_element_1st_line, '-->' ); 128 | $block_element_1st_line_patched = substr_replace( $block_element_1st_line, '{"' . $attribute_name . '":"' . $attribute_value . '"} -->', $pos_close_comment, $length = 3 ); 129 | } 130 | 131 | $block_element_lines[0] = $block_element_1st_line_patched; 132 | $block_element_patched = implode( "\n", $block_element_lines ); 133 | 134 | return $block_element_patched; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/unit/content-patcher/patchers/test-video-patcher.php: -------------------------------------------------------------------------------- 1 | patcher = new VideoPatcher(); 39 | $this->data_provider = new DataProviderVideoPatcher(); 40 | } 41 | 42 | /** 43 | * The patcher should patch the lost video element. 44 | */ 45 | public function test_should_patch_lost_video_element() { 46 | $html = $this->data_provider->get_lost_video_html(); 47 | $blocks_before_patching = $this->data_provider->get_lost_video_blocks_before_patching(); 48 | $expected = $this->data_provider->get_lost_video_blocks_patched_expected(); 49 | 50 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 51 | 52 | $this->assertSame( $expected, $actual ); 53 | } 54 | 55 | /** 56 | * Tests that blocks code will not get modified if running into an inconsistency btw. HTML and non-patched blocks code. 57 | */ 58 | public function test_should_not_modify_source_if_html_is_html_code_inconsistent_with_blocks_code() { 59 | $html = $this->data_provider->get_inconsistent_sources_html(); 60 | $blocks_before_patching = $this->data_provider->get_inconsistent_sources_before_patching(); 61 | $expected = $this->data_provider->get_inconsistent_sources_blocks_patched_expected(); 62 | 63 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 64 | 65 | $this->assertSame( $expected, $actual ); 66 | } 67 | 68 | /** 69 | * Tests that blocks code will not get modified if HTML code is not supposed to be patched by this patcher. 70 | */ 71 | public function test_should_not_modify_source_if_html_not_pertinent_to_this_patcher() { 72 | $html = $this->data_provider->get_html_is_non_pertinent_html(); 73 | $blocks_before_patching = $this->data_provider->get_html_is_non_pertinent_before_patching(); 74 | $expected = $this->data_provider->get_html_is_non_pertinent_blocks_patched_expected(); 75 | 76 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 77 | 78 | $this->assertSame( $expected, $actual ); 79 | } 80 | 81 | /** 82 | * Should patch multiple videos. 83 | */ 84 | public function test_should_patch_multiple_video_elements() { 85 | $html = $this->data_provider->get_multiple_videos_html(); 86 | $blocks_before_patching = $this->data_provider->get_multiple_video_blocks_before_patching(); 87 | $expected = $this->data_provider->get_multiple_video_blocks_patched_expected(); 88 | 89 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 90 | 91 | $this->assertSame( $expected, $actual ); 92 | } 93 | 94 | /** 95 | * This is a case where not all videos have the src attribute. Patcher should not patch those that don't. 96 | */ 97 | public function test_should_skip_patching_videos_which_dont_have_a_valid_src_attribute() { 98 | $html = $this->data_provider->get_some_skipped_videos_html(); 99 | $blocks_before_patching = $this->data_provider->get_some_skipped_videos_blocks_before_patching(); 100 | $expected = $this->data_provider->get_some_skipped_videos_blocks_patched_expected(); 101 | 102 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 103 | 104 | $this->assertSame( $expected, $actual ); 105 | } 106 | 107 | /** 108 | * This is a case where not all video elements are valid. Patcher should not patch those. 109 | */ 110 | public function test_should_skip_patching_invalid_videos() { 111 | $html = $this->data_provider->get_some_invalid_videos_html(); 112 | $blocks_before_patching = $this->data_provider->get_some_invalid_videos_blocks_before_patching(); 113 | $expected = $this->data_provider->get_some_invalid_videos_blocks_patched_expected(); 114 | 115 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 116 | 117 | $this->assertSame( $expected, $actual ); 118 | } 119 | 120 | /** 121 | * This test will become important when Gutenberg fixes conversion of this particular case, so we need to make sure 122 | * we won't patch it twice. 123 | */ 124 | public function test_should_skip_patching_videos_that_already_have_the_dir_attribute() { 125 | $html = $this->data_provider->get_some_videos_ok_html(); 126 | $blocks_before_patching = $this->data_provider->get_some_videos_ok_blocks_before_patching(); 127 | $expected = $this->data_provider->get_some_videos_ok_blocks_patched_expected(); 128 | 129 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 130 | 131 | $this->assertSame( $expected, $actual ); 132 | } 133 | 134 | /** 135 | * Runs a comprehensive example and checks for validity. 136 | */ 137 | public function test_should_correctly_patch_a_comprehensive_video_conversion_example() { 138 | $html = $this->data_provider->get_comprehensive_html(); 139 | $blocks_before_patching = $this->data_provider->get_comprehensive_blocks_before_patching(); 140 | $expected = $this->data_provider->get_comprehensive_blocks_patched_expected(); 141 | 142 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 143 | 144 | $this->assertSame( $expected, $actual ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/unit/content-patcher/patchers/test-audio-patcher.php: -------------------------------------------------------------------------------- 1 | patcher = new AudioPatcher(); 39 | $this->data_provider = new DataProviderAudioPatcher(); 40 | } 41 | 42 | /** 43 | * The patcher should patch the lost audio element. 44 | */ 45 | public function test_should_patch_lost_audio_element() { 46 | $html = $this->data_provider->get_lost_audio_html(); 47 | $blocks_before_patching = $this->data_provider->get_lost_audio_blocks_before_patching(); 48 | $expected = $this->data_provider->get_lost_audio_blocks_patched_expected(); 49 | 50 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 51 | 52 | $this->assertSame( $expected, $actual ); 53 | } 54 | 55 | /** 56 | * Tests that blocks code will not get modified if running into an inconsistency btw. HTML and non-patched blocks code. 57 | */ 58 | public function test_should_not_modify_source_if_html_is_html_code_inconsistent_with_blocks_code() { 59 | $html = $this->data_provider->get_inconsistent_sources_html(); 60 | $blocks_before_patching = $this->data_provider->get_inconsistent_sources_before_patching(); 61 | $expected = $this->data_provider->get_inconsistent_sources_blocks_patched_expected(); 62 | 63 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 64 | 65 | $this->assertSame( $expected, $actual ); 66 | } 67 | 68 | /** 69 | * Tests that blocks code will not get modified if HTML code is not supposed to be patched by this patcher. 70 | */ 71 | public function test_should_not_modify_source_if_html_not_pertinent_to_this_patcher() { 72 | $html = $this->data_provider->get_html_is_non_pertinent_html(); 73 | $blocks_before_patching = $this->data_provider->get_html_is_non_pertinent_before_patching(); 74 | $expected = $this->data_provider->get_html_is_non_pertinent_blocks_patched_expected(); 75 | 76 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 77 | 78 | 79 | $this->assertSame( $expected, $actual ); 80 | } 81 | 82 | /** 83 | * Should patch multiple audios. 84 | */ 85 | public function test_should_patch_multiple_audio_elements() { 86 | $html = $this->data_provider->get_multiple_audios_html(); 87 | $blocks_before_patching = $this->data_provider->get_multiple_audio_blocks_before_patching(); 88 | $expected = $this->data_provider->get_multiple_audio_blocks_patched_expected(); 89 | 90 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 91 | 92 | $this->assertSame( $expected, $actual ); 93 | } 94 | 95 | /** 96 | * This is a case where not all audios have the src attribute. Patcher should not patch those that don't. 97 | */ 98 | public function test_should_skip_patching_audios_which_dont_have_a_valid_src_attribute() { 99 | $html = $this->data_provider->get_some_skipped_audios_html(); 100 | $blocks_before_patching = $this->data_provider->get_some_skipped_audios_blocks_before_patching(); 101 | $expected = $this->data_provider->get_some_skipped_audios_blocks_patched_expected(); 102 | 103 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 104 | 105 | $this->assertSame( $expected, $actual ); 106 | } 107 | 108 | /** 109 | * This is a case where not all audio elements are valid. Patcher should not patch those. 110 | */ 111 | public function test_should_skip_patching_invalid_audios() { 112 | $html = $this->data_provider->get_some_invalid_audios_html(); 113 | $blocks_before_patching = $this->data_provider->get_some_invalid_audios_blocks_before_patching(); 114 | $expected = $this->data_provider->get_some_invalid_audios_blocks_patched_expected(); 115 | 116 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 117 | 118 | $this->assertSame( $expected, $actual ); 119 | } 120 | 121 | /** 122 | * This test will become important when Gutenberg fixes conversion of this particular case, so we need to make sure 123 | * we won't patch it twice. 124 | */ 125 | public function test_should_skip_patching_audios_that_already_have_the_dir_attribute() { 126 | $html = $this->data_provider->get_some_audios_ok_html(); 127 | $blocks_before_patching = $this->data_provider->get_some_audios_ok_blocks_before_patching(); 128 | $expected = $this->data_provider->get_some_audios_ok_blocks_patched_expected(); 129 | 130 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 131 | 132 | $this->assertSame( $expected, $actual ); 133 | } 134 | 135 | /** 136 | * Runs a comprehensive example and checks for validity. 137 | */ 138 | public function test_should_correctly_patch_a_comprehensive_audio_conversion_example() { 139 | $html = $this->data_provider->get_comprehensive_html(); 140 | $blocks_before_patching = $this->data_provider->get_comprehensive_blocks_before_patching(); 141 | $expected = $this->data_provider->get_comprehensive_blocks_patched_expected(); 142 | 143 | $actual = $this->patcher->patch_blocks_contents( $blocks_before_patching, $html, 1 ); 144 | 145 | $this->assertSame( $expected, $actual ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /assets/src/conversion/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component, Fragment } from '@wordpress/element'; 5 | import { __ } from '@wordpress/i18n'; 6 | import { 7 | Button, 8 | Card, 9 | CardBody, 10 | CardFooter, 11 | Notice, 12 | TextControl 13 | } from '@wordpress/components'; 14 | 15 | /** 16 | * Newspack dependencies. 17 | */ 18 | import { NewspackIcon } from 'newspack-components'; 19 | 20 | /** 21 | * Internal dependencies. 22 | */ 23 | import { 24 | fetchConversionInfo, 25 | fetchPrepareConversion, 26 | fetchResetConversion, 27 | downloadListConvertedIds, 28 | downloadListUnsuccessfullyConvertedIds, 29 | } from '../utilities'; 30 | 31 | class Conversion extends Component { 32 | constructor( props ) { 33 | super( props ); 34 | 35 | this.state = { 36 | isConversionPrepared: false, 37 | unconvertedCount: '...', 38 | totalNumberOfBatches: '...', 39 | areThereSuccessfullyConvertedIds: false, 40 | areThereUnconvertedIds: false, 41 | minIdToProcess: -1, 42 | maxIdToProcess: -1, 43 | }; 44 | } 45 | 46 | componentDidMount() { 47 | return fetchConversionInfo().then( response => { 48 | if ( response ) { 49 | const { 50 | isConversionPrepared, 51 | unconvertedCount, 52 | totalNumberOfBatches, 53 | areThereSuccessfullyConvertedIds, 54 | areThereUnconvertedIds, 55 | minIdToProcess, 56 | maxIdToProcess, 57 | } = response; 58 | this.setState( { 59 | isConversionPrepared, 60 | unconvertedCount, 61 | totalNumberOfBatches, 62 | areThereSuccessfullyConvertedIds, 63 | areThereUnconvertedIds, 64 | minIdToProcess, 65 | maxIdToProcess, 66 | } ); 67 | } 68 | return new Promise( ( resolve, reject ) => resolve() ); 69 | } ); 70 | } 71 | 72 | handleOnClickRunConversion = () => { 73 | return fetchPrepareConversion().then( response => { 74 | if ( response && response.success ) { 75 | window.parent.location = '/wp-admin/post-new.php?newspack-content-converter'; 76 | } 77 | } ); 78 | }; 79 | 80 | handleDownloadListConverted = () => { 81 | downloadListConvertedIds(); 82 | }; 83 | 84 | handleDownloadListUnsuccessfullyConverted = () => { 85 | downloadListUnsuccessfullyConvertedIds(); 86 | }; 87 | 88 | handleOnClickResetConversion = () => { 89 | return fetchResetConversion().then( response => { 90 | if ( response ) { 91 | location.reload(); 92 | } 93 | } ); 94 | }; 95 | 96 | render() { 97 | const { 98 | isConversionPrepared, 99 | unconvertedCount, 100 | totalNumberOfBatches, 101 | areThereSuccessfullyConvertedIds, 102 | areThereUnconvertedIds, 103 | minIdToProcess, 104 | maxIdToProcess, 105 | } = this.state; 106 | if ( '1' == isConversionPrepared ) { 107 | return ( 108 | 109 |
110 | 111 |

{ __( 'Content Converter / Converting...' ) }

112 |
113 | 114 | 115 | 116 | { __( 117 | 'Conversion of your content has already been started in a designated browser tab. In case it was terminated or closed unexpectedly, you can reset the conversion here and resume converting again.' 118 | ) } 119 | 120 |

121 | { __( 122 | 'Before attempting to see results on this page or to convert again, wait for the ongoing conversion to finish up.' 123 | ) } 124 |

125 |
126 | 127 | 130 | 131 |
132 |
133 | ); 134 | } else { 135 | return ( 136 | 137 |
138 | 139 |

{ __( 'Content Converter' ) }

140 |
141 | 142 | 143 | 144 | { __( 145 | 'Once started, the conversion should not be interrupted! Your browser tab needs to remain active until conversion is complete.' 146 | ) } 147 | 148 |

149 | { __( 'Conversion permanently modifies content so it is recommended to perform a full database backup before running it.' ) } 150 |

151 | 156 | 161 |
162 | { ( maxIdToProcess > 0 || minIdToProcess > 0 )&& ( 163 | 164 | { ( minIdToProcess > 0 ) && (

{ sprintf( __( 'Min post ID to process is set to %d' ), minIdToProcess) }

) } 165 | { ( maxIdToProcess > 0 ) && (

{ sprintf( __( 'Max post ID to process is set to %d' ), maxIdToProcess) }

) } 166 |
167 | ) } 168 | { ( areThereSuccessfullyConvertedIds || areThereUnconvertedIds )&& ( 169 | 170 | { areThereSuccessfullyConvertedIds && ( 171 | { __( 'Download IDs of all converted entries' ) } 172 | ) } 173 | { areThereSuccessfullyConvertedIds && areThereUnconvertedIds && ( 174 |
175 | ) } 176 | { areThereUnconvertedIds && ( 177 | { __( 'Download IDs of unconverted entries' ) } 178 | ) } 179 |
180 | ) } 181 | 182 | 185 | 186 |
187 |
188 | ); 189 | } 190 | } 191 | } 192 | 193 | export default Conversion; 194 | -------------------------------------------------------------------------------- /lib/content-patcher/patchers/class-audiopatcher.php: -------------------------------------------------------------------------------- 1 | square_brackets_element_manipulator = new SquareBracketsElementManipulator(); 49 | $this->wp_block_manipulator = new WpBlockManipulator(); 50 | $this->html_element_manipulator = new HtmlElementManipulator(); 51 | } 52 | 53 | /** 54 | * See the \NewspackContentConverter\ContentPatcher\Patchers\PatcherInterface::patch_blocks_contents for description. 55 | * 56 | * @param string $source_blocks Block content as result of Gutenberg "conversion to blocks". 57 | * @param string $source_html HTML source, original content being converted. 58 | * @param int $post_id Post ID. 59 | * 60 | * @return string|false 61 | */ 62 | public function patch_blocks_contents( $source_blocks, $source_html, $post_id ) { 63 | 64 | $matches_html = $this->square_brackets_element_manipulator->match_elements_with_closing_tags( 'audio', $source_html ); 65 | if ( ! $matches_html ) { 66 | // TODO: DEBUG LOG 'no elements matched in HTML'. 67 | return $source_blocks; 68 | } 69 | 70 | $matches_blocks = $this->wp_block_manipulator->match_wp_block( 'wp:audio', $source_blocks ); 71 | if ( is_null( $matches_blocks ) ) { 72 | return $source_blocks; 73 | } 74 | 75 | if ( ! $this->validate_html_and_block_matches( $matches_html[0], $matches_blocks[0] ) ) { 76 | // TODO: DEBUG LOG 'HTML and block matches do not correspond'. 77 | return $source_blocks; 78 | } 79 | 80 | // Applying array_reverse() on matched results, because when iterating over them, the patcher might apply several patches, 81 | // and the easiest way to preserve the positions of all the strings which are being replaced, is to just patch (replace) 82 | // from end to start. 83 | $matches_html[0] = array_reverse( $matches_html[0] ); 84 | $matches_blocks[0] = array_reverse( $matches_blocks[0] ); 85 | 86 | foreach ( $matches_html[0] as $key => $match_html ) { 87 | $html_element = $match_html[0]; 88 | $position_html_element = $match_html[1]; 89 | $blocks_element = $matches_blocks[0][ $key ][0]; 90 | $position_blocks_element = $matches_blocks[0][ $key ][1]; 91 | 92 | $patched_block_element = $this->patch_audio_src_attribute( $html_element, $blocks_element ); 93 | if ( $patched_block_element ) { 94 | $source_blocks = substr_replace( $source_blocks, $patched_block_element, $position_blocks_element, strlen( $blocks_element ) ); 95 | } 96 | } 97 | 98 | return $source_blocks; 99 | } 100 | 101 | /** 102 | * Patches the audio src attribute, by searching for it in the HTML element, then applying it to the block element. 103 | * 104 | * @param string $html_element HTML element. 105 | * @param string $block_element Block element. 106 | * 107 | * @return string|false Updated block element, or false. 108 | */ 109 | private function patch_audio_src_attribute( $html_element, $block_element ) { 110 | 111 | // Extract the specific src attribute from HTML [audio][/audio] element. 112 | // Different possible names of the src attributes: https://en.support.wordpress.com/accepted-filetypes/#audio . 113 | $possible_src_attributes = [ 'mp3', 'm4a', 'ogg', 'wav' ]; 114 | 115 | foreach ( $possible_src_attributes as $attribute_name ) { 116 | $attribute_value = $this->square_brackets_element_manipulator->get_attribute_value( $attribute_name, $html_element ); 117 | if ( $attribute_value ) { 118 | break; 119 | } 120 | } 121 | 122 | if ( ! $attribute_value ) { 123 | // TODO: DEBUG LOG 'no src audio matched in HTML'. 124 | return false; 125 | } 126 | 127 | // The found src is to be patched as a new