From f1bb4513a8630e3dbf0c081df805ee2a2cc62768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Dumont?= Date: Mon, 5 Aug 2024 00:33:38 +0200 Subject: [PATCH] v2.3.0 release --- CHANGELOG.md | 19 +- cocart-beta-tester.php | 4 +- composer.json | 29 +- .../class-cocart-beta-tester-admin-assets.php | 1 - .../class-cocart-beta-tester-admin-menus.php | 1 - includes/class-cocart-beta-tester-channel.php | 11 +- includes/class-cocart-beta-tester-core.php | 8 +- ...class-cocart-beta-tester-plugin-update.php | 45 +- ...ass-cocart-beta-tester-plugin-upgrader.php | 1 - ...lass-cocart-beta-tester-version-picker.php | 18 +- includes/class-cocart-beta-tester.php | 13 +- .../html-admin-notice-missing-cocart.php | 17 +- package.json | 43 +- parsedown.php | 3136 ++++++++--------- phpcs.xml | 17 +- 15 files changed, 1617 insertions(+), 1746 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b848d..7adbed7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Changelog for CoCart Beta Tester +## v2.3.0 - 5th August, 2024 + +* Switch Version: Changelogs linked per version now link to the next changelog if the version is not stable. +* Misc: Various stuff like links updated and fixed. + ## v2.2.0 - 25th January, 2023 -* Enhancement - Channel selection updated; Now you can choose to receive nightly builds. +* Enhancement: Channel selection updated; Now you can choose to receive nightly builds. ## v2.1.0 - 21st January, 2022 @@ -12,12 +17,12 @@ ## v2.0.0 - 20th October, 2021 -* Enhancement - Channel selection; choose to receive RC or beta versions. -* Enhancement - Admin bar item shows version information, and offers shortcuts to functionality. -* Enhancement - Shortcut to log GitHub issues. -* Enhancement - Version switcher; choose which release or prerelease to switch to. -* Enhancement - Setting to enable auto-updates. +* Enhancement: Channel selection; choose to receive RC or beta versions. +* Enhancement: Admin bar item shows version information, and offers shortcuts to functionality. +* Enhancement: Shortcut to log GitHub issues. +* Enhancement: Version switcher; choose which release or prerelease to switch to. +* Enhancement: Setting to enable auto-updates. ## v1.0.0 - 8th August, 2020 -* Initial release. \ No newline at end of file +* Initial release. diff --git a/cocart-beta-tester.php b/cocart-beta-tester.php index 0ad0c32..b3c5d09 100644 --- a/cocart-beta-tester.php +++ b/cocart-beta-tester.php @@ -49,7 +49,7 @@ function _ccbt_load_text_domain() { */ function cocart_beta_tester() { if ( ! defined( 'COCART_VERSION' ) ) { - add_action( 'admin_notices', function() { + add_action( 'admin_notices', function () { include_once untrailingslashit( plugin_dir_path( COCART_BETA_TESTER_FILE ) ) . '/includes/views/html-admin-notice-missing-cocart.php'; } ); } elseif ( ! class_exists( 'CoCart_Beta_Tester' ) ) { @@ -59,7 +59,7 @@ function cocart_beta_tester() { } } - add_action( 'plugins_loaded', function(){ + add_action( 'plugins_loaded', function () { cocart_beta_tester(); }, 99 ); } diff --git a/composer.json b/composer.json index 9dfc01e..e86a0e8 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { - "name": "co-cart/cocart-beta-tester", + "name": "cocart-headless/cocart-beta-tester", "description": "Easily update to prerelease versions of CoCart Lite for testing and development purposes.", - "homepage": "https://cocart.xyz", + "homepage": "https://cocartapi.com", "type": "wordpress-plugin", "keywords": [ "wordpress", @@ -15,14 +15,14 @@ "minimum-stability": "stable", "authors": [ { - "name": "Sébastien Dumont", - "email": "mailme@sebastiendumont.com", - "homepage": "https://sebastiendumont.com", - "role": "Developer" + "name": "CoCart Headless, LLC", + "email": "support@cocartapi.com", + "homepage": "https://cocartapi.com", + "role": "Owner" } ], "support": { - "issues": "https://github.com/co-cart/cocart-beta-tester/issues" + "issues": "https://github.com/cocart-headless/cocart-beta-tester/issues" }, "autoload": { "classmap": [ @@ -30,12 +30,11 @@ ] }, "require": { - "composer/installers": "1.11.0", - "co-cart/cocart-sniffs": "0.0.2" + "composer/installers": "^2.0" }, "require-dev": { - "wp-cli/i18n-command": "2.2.9", - "squizlabs/php_codesniffer": "3.6.0" + "cocart-headless/cocart-sniffs": "0.0.3", + "automattic/vipwpcs": "^3.0" }, "scripts": { "phpcs": [ @@ -53,5 +52,11 @@ "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" } + }, + "config": { + "allow-plugins": { + "composer/installers": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } } -} \ No newline at end of file +} diff --git a/includes/class-cocart-beta-tester-admin-assets.php b/includes/class-cocart-beta-tester-admin-assets.php index 8279fd8..f5a0c75 100644 --- a/includes/class-cocart-beta-tester-admin-assets.php +++ b/includes/class-cocart-beta-tester-admin-assets.php @@ -51,7 +51,6 @@ public function admin_scripts() { wp_enqueue_script( 'cc-beta-tester-version-picker' ); } } // END admin_scripts() - } // END class return new CoCart_Beta_Tester_Admin_Assets(); diff --git a/includes/class-cocart-beta-tester-admin-menus.php b/includes/class-cocart-beta-tester-admin-menus.php index 8ba34c5..b45cf4f 100644 --- a/includes/class-cocart-beta-tester-admin-menus.php +++ b/includes/class-cocart-beta-tester-admin-menus.php @@ -139,7 +139,6 @@ public function hide_from_menus() { } } } // END hide_from_menus() - } // END class return new CoCart_Beta_Tester_Admin_Menus(); diff --git a/includes/class-cocart-beta-tester-channel.php b/includes/class-cocart-beta-tester-channel.php index 362e89f..89cc9de 100644 --- a/includes/class-cocart-beta-tester-channel.php +++ b/includes/class-cocart-beta-tester-channel.php @@ -73,7 +73,7 @@ public function update_section_html( $args ) {

-

', '' ); ?>

+

', '' ); ?>

array( + 'nightly' => array( 'name' => __( 'Nightly Releases', 'cocart-beta-tester' ), 'description' => __( 'Nightly releases contain experimental functionality for testing purposes only. This channel will include features that are unstable and may not move forward into a beta release.', 'cocart-beta-tester' ), ), - 'beta' => array( + 'beta' => array( 'name' => __( 'Beta Releases', 'cocart-beta-tester' ), 'description' => __( 'Beta releases contain upcoming features for testing purposes only. This channel will also include RC and stable releases if more current.', 'cocart-beta-tester' ), ), - 'rc' => array( + 'rc' => array( 'name' => __( 'Release Candidates', 'cocart-beta-tester' ), 'description' => __( 'Release candidates are released to ensure any critical problems have not gone undetected. This channel will also include stable releases if more current.', 'cocart-beta-tester' ), ), - 'stable' => array( + 'stable' => array( 'name' => __( 'Stable Releases', 'cocart-beta-tester' ), 'description' => __( 'This is the default behavior in WordPress.', 'cocart-beta-tester' ), ), @@ -178,7 +178,6 @@ public function settings_page_html() { plugin_file = 'cart-rest-api-for-woocommerce/cart-rest-api-for-woocommerce.php'; - $this->plugin_slug = 'cart-rest-api-for-woocommerce'; - parent::__construct( $this->plugin_file, $this->plugin_slug ); } // END__construct() - } // END class return new CoCart_Beta_Tester_Core(); diff --git a/includes/class-cocart-beta-tester-plugin-update.php b/includes/class-cocart-beta-tester-plugin-update.php index a9eac67..34ac948 100644 --- a/includes/class-cocart-beta-tester-plugin-update.php +++ b/includes/class-cocart-beta-tester-plugin-update.php @@ -35,6 +35,13 @@ class CoCart_Beta_Tester_Plugin_Update { */ public $api_url; + /** + * Plugin Data + * + * @var string + */ + public $plugin_data; + /** * Constructor. * @@ -135,7 +142,7 @@ public function api_check( $transient ) { 'slug' => $plugin_slug, 'plugin' => $filename, 'new_version' => $version, - 'url' => 'https://cocart.xyz', + 'url' => 'https://cocartapi.com', 'package' => '', 'icons' => array( '2x' => esc_url( 'https://raw.githubusercontent.com/co-cart/co-cart/trunk/.wordpress-org/icon-256x256.png' ), @@ -335,7 +342,6 @@ public function get_latest_channel_release() { $tagged_version = get_site_transient( md5( $this->plugin_slug ) . '_latest_tag' ); if ( $this->overrule_transients() || empty( $tagged_version ) ) { - $versions = $this->get_data(); $versions = $this->sort_release_order( $versions, true ); $channel = CoCart_Beta_Tester::get_settings()->channel; @@ -391,7 +397,7 @@ public function sort_release_order( $versions, $reverse_order = false ) { $new_order[ $version->tag_name ] = $version; } - usort( $new_order, function( $a, $b ) { + usort( $new_order, function ( $a, $b ) { return -1 * version_compare( $a->tag_name, $b->tag_name ); }); @@ -540,7 +546,7 @@ public function get_plugin_data() { /** * Return true if version string is a nightly version. * - * @access protected + * @access public * * @static * @@ -548,14 +554,14 @@ public function get_plugin_data() { * * @return bool */ - protected static function is_nightly_version( $version_str ) { + public static function is_nightly_version( $version_str ) { return strpos( $version_str, 'nightly' ) !== false || strpos( $version_str, 'nb' ) !== false || strpos( $version_str, 'night-build' ) !== false; } // END is_nightly_version() /** * Return true if version string is a beta version. * - * @access protected + * @access public * * @static * @@ -563,14 +569,14 @@ protected static function is_nightly_version( $version_str ) { * * @return bool */ - protected static function is_beta_version( $version_str ) { + public static function is_beta_version( $version_str ) { return strpos( $version_str, 'beta' ) !== false; } // END is_beta_version() /** * Return true if version string is a Release Candidate. * - * @access protected + * @access public * * @static * @@ -578,14 +584,14 @@ protected static function is_beta_version( $version_str ) { * * @return bool */ - protected static function is_rc_version( $version_str ) { + public static function is_rc_version( $version_str ) { return strpos( $version_str, 'rc' ) !== false || strpos( $version_str, 'RC' ) !== false; } // END is_rc_version() /** * Return true if version string is a stable version. * - * @access protected + * @access public * * @static * @@ -593,7 +599,7 @@ protected static function is_rc_version( $version_str ) { * * @return bool */ - protected static function is_stable_version( $version_str ) { + public static function is_stable_version( $version_str ) { return ! self::is_beta_version( $version_str ) && ! self::is_nightly_version( $version_str ) && ! self::is_rc_version( $version_str ); } // END is_stable_version() @@ -601,7 +607,7 @@ protected static function is_stable_version( $version_str ) { * Return true if release's version string belongs to beta channel, i.e. * if it's beta, rc or stable release. * - * @access protected + * @access public * * @static * @@ -609,14 +615,14 @@ protected static function is_stable_version( $version_str ) { * * @return bool */ - protected static function is_in_beta_channel( $version_str ) { + public static function is_in_beta_channel( $version_str ) { return self::is_beta_version( $version_str ) || self::is_rc_version( $version_str ) || self::is_stable_version( $version_str ); } // END is_in_beta_channel() /** * Return true if release's version string belongs to nightly channel. * - * @access protected + * @access public * * @static * @@ -624,7 +630,7 @@ protected static function is_in_beta_channel( $version_str ) { * * @return bool */ - protected static function is_in_nightly_channel( $version_str ) { + public static function is_in_nightly_channel( $version_str ) { return self::is_nightly_version( $version_str ); } // END is_in_nightly_channel() @@ -632,7 +638,7 @@ protected static function is_in_nightly_channel( $version_str ) { * Return true if release's version string belongs to release candidate channel, i.e. * if it's rc or stable release. * - * @access protected + * @access public * * @static * @@ -640,7 +646,7 @@ protected static function is_in_nightly_channel( $version_str ) { * * @return bool */ - protected static function is_in_rc_channel( $version_str ) { + public static function is_in_rc_channel( $version_str ) { return self::is_rc_version( $version_str ) || self::is_stable_version( $version_str ); } // END is_in_rc_channel() @@ -648,7 +654,7 @@ protected static function is_in_rc_channel( $version_str ) { * Return true if release's version string belongs to stable channel, i.e. * if it's stable release and not a beta or rc. * - * @access protected + * @access public * * @static * @@ -656,7 +662,7 @@ protected static function is_in_rc_channel( $version_str ) { * * @return bool */ - protected static function is_in_stable_channel( $version_str ) { + public static function is_in_stable_channel( $version_str ) { return self::is_stable_version( $version_str ); } // END is_in_stable_channel() @@ -702,5 +708,4 @@ public function get_tags( $channel = 'all' ) { return $tags; } // END get_tags() - } // END class diff --git a/includes/class-cocart-beta-tester-plugin-upgrader.php b/includes/class-cocart-beta-tester-plugin-upgrader.php index e9276d6..045363b 100644 --- a/includes/class-cocart-beta-tester-plugin-upgrader.php +++ b/includes/class-cocart-beta-tester-plugin-upgrader.php @@ -71,5 +71,4 @@ public function switch_version( $plugin, $args = array() ) { return true; } // END switch_version() - } // END class diff --git a/includes/class-cocart-beta-tester-version-picker.php b/includes/class-cocart-beta-tester-version-picker.php index 9d89d7e..258fcb0 100644 --- a/includes/class-cocart-beta-tester-version-picker.php +++ b/includes/class-cocart-beta-tester-version-picker.php @@ -71,7 +71,7 @@ public function handle_version_switch() { } try { - include dirname( __FILE__ ) . '/class-cocart-beta-tester-plugin-upgrader.php'; + include __DIR__ . '/class-cocart-beta-tester-plugin-upgrader.php'; $plugin_name = $this->plugin_slug; $plugin = $this->plugin_file; @@ -182,10 +182,17 @@ public function get_versions_html( $channel ) { $versions_html .= '' . esc_html__( ' Installed Version', 'cocart-beta-tester' ) . ''; } - $versions_html .= sprintf( - ' - ' . __( 'Changelog', 'cocart-beta-tester' ) . '', - 'https://github.com/co-cart/co-cart/blob/' . $tag->tag_name . '/CHANGELOG.md' - ); + if ( ! $updates->is_stable_version( $tag_version ) ) { + $versions_html .= sprintf( + ' - ' . __( 'Changelog', 'cocart-beta-tester' ) . '', + 'https://github.com/co-cart/co-cart/blob/' . $tag->tag_name . '/NEXT_CHANGELOG.md' + ); + } else { + $versions_html .= sprintf( + ' - ' . __( 'Changelog', 'cocart-beta-tester' ) . '', + 'https://github.com/co-cart/co-cart/blob/' . $tag->tag_name . '/CHANGELOG.md' + ); + } $versions_html .= ''; $versions_html .= ''; @@ -285,7 +292,6 @@ public function select_versions_form_html() {

%2$s', esc_url( wp_nonce_url( self_admin_url( add_query_arg( array( + printf( '%2$s', esc_url( wp_nonce_url( self_admin_url( add_query_arg( array( 'action' => 'activate', 'plugin' => 'cart-rest-api-for-woocommerce/cart-rest-api-for-woocommerce.php', 'plugin_status' => 'active', ), 'plugins.php' ) ), 'activate-plugin_cart-rest-api-for-woocommerce/cart-rest-api-for-woocommerce.php' ) ), sprintf( esc_html__( 'Activate %s', 'cocart-beta-tester' ), 'CoCart' ) ); - else : - - echo sprintf( esc_html__( 'As you do not have permission to activate a plugin. Please ask a site administrator to activate %s for you.', 'cocart-beta-tester' ), 'CoCart' ); - + printf( esc_html__( 'As you do not have permission to activate a plugin. Please ask a site administrator to activate %s for you.', 'cocart-beta-tester' ), 'CoCart' ); endif; - else : - if ( current_user_can( 'install_plugins' ) ) { $url = wp_nonce_url( self_admin_url( add_query_arg( array( 'action' => 'install-plugin', @@ -57,11 +50,10 @@ } echo '' . sprintf( esc_html__( 'Install %s', 'cocart-beta-tester' ), 'CoCart' ) . ''; - endif; if ( current_user_can( 'deactivate_plugin', 'cocart-pro/cocart-pro.php' ) ) : - echo sprintf( + printf( ' %2$s', esc_url( wp_nonce_url( self_admin_url( add_query_arg( array( 'action' => 'deactivate', @@ -69,7 +61,6 @@ ), 'plugins.php' ) ), 'deactivate-plugin_cocart-beta-tester/cocart-beta-tester.php' ) ), esc_html__( 'Turn off Beta Tester', 'cocart-beta-tester' ) ); - endif; ?>

diff --git a/package.json b/package.json index 8576131..2de88a8 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,30 @@ { "title": "CoCart - Beta Tester", "name": "cocart-beta-tester", - "version": "2.2.0", + "version": "2.3.0", "private": false, "contributors": [ "sebd86", "cocartforwc" ], - "author": "Sébastien Dumont", - "author_uri": "https://sebastiendumont.com", - "homepage": "https://cocart.xyz/", + "author": "CoCart Headless, LLC", + "author_uri": "https://cocartapi.com", + "homepage": "https://cocartapi.com", "main": "Gruntfile.js", "repository": { "type": "git", - "url": "https://github.com/co-cart/cocart-beta-tester.git" + "url": "https://github.com/cocart-headless/cocart-beta-tester.git" }, "bugs": { - "url": "https://github.com/co-cart/cocart-beta-tester/issues" + "url": "https://github.com/cocart-headless/cocart-beta-tester/issues" }, - "requires": "5.5", + "requires": "5.6", "requires_php": "7.4", - "tested_up_to": "6.1", + "tested_up_to": "6.6", + "cocart_requires": "4.0", + "cocart_tested_up_to": "4.3", "license": "GPL-3.0", - "copyright": "Copyright (c) 2023, Sébastien Dumont", + "copyright": "Copyright (c) 2024, CoCart Headless, LLC", "description": "Easily update to prerelease versions of CoCart for testing and development purposes.", "tags": "", "keywords": [ @@ -42,25 +44,18 @@ "headless-woocommerce" ], "scripts": { - "test": "grunt", - "update:version": "grunt version", - "zip:release": "grunt zip", "packages:update": "npm install -g npm-check-updates & ncu -u & npm update & npm install" }, "devDependencies": { - "grunt": "1.5.3", + "grunt": "~1.6.1", "grunt-checktextdomain": "1.0.1", - "grunt-contrib-clean": "2.0.1", - "grunt-contrib-compress": "2.0.0", - "grunt-contrib-copy": "1.0.0", - "grunt-contrib-jshint": "3.2.0", - "grunt-contrib-uglify": "5.2.2", - "grunt-dev-update": "2.3.0", - "grunt-newer": "1.3.0", - "grunt-potomo": "3.5.0", - "grunt-text-replace": "0.4.0", - "grunt-wp-i18n": "1.0.3", - "load-grunt-tasks": "5.1.0" + "grunt-contrib-clean": "^2.0.1", + "grunt-contrib-compress": "^2.0.0", + "grunt-contrib-copy": "^1.0.0", + "grunt-newer": "^1.3.0", + "grunt-text-replace": "^0.4.0", + "grunt-wp-i18n": "~1.0.3", + "load-grunt-tasks": "~5.1.0" }, "dependencies": {}, "engines": { diff --git a/parsedown.php b/parsedown.php index 6843635..75a36a2 100644 --- a/parsedown.php +++ b/parsedown.php @@ -1,1693 +1,1553 @@ DefinitionData = array(); - - # standardize line breaks - $text = str_replace(array("\r\n", "\r"), "\n", $text); - - # remove surrounding line breaks - $text = trim($text, "\n"); - - # split text into lines - $lines = explode("\n", $text); - - # iterate through lines to identify blocks - $markup = $this->lines($lines); - - # trim line breaks - $markup = trim($markup, "\n"); - - return $markup; - } - - # - # Setters - # - - function setBreaksEnabled($breaksEnabled) - { - $this->breaksEnabled = $breaksEnabled; - - return $this; - } - - protected $breaksEnabled; - - function setMarkupEscaped($markupEscaped) - { - $this->markupEscaped = $markupEscaped; - - return $this; - } - - protected $markupEscaped; - - function setUrlsLinked($urlsLinked) - { - $this->urlsLinked = $urlsLinked; - - return $this; - } - - protected $urlsLinked = true; - - function setSafeMode($safeMode) - { - $this->safeMode = (bool) $safeMode; - - return $this; - } - - protected $safeMode; - - protected $safeLinksWhitelist = array( - 'http://', - 'https://', - 'ftp://', - 'ftps://', - 'mailto:', - 'data:image/png;base64,', - 'data:image/gif;base64,', - 'data:image/jpeg;base64,', - 'irc:', - 'ircs:', - 'git:', - 'ssh:', - 'news:', - 'steam:', - ); - - # - # Lines - # - - protected $BlockTypes = array( - '#' => array('Header'), - '*' => array('Rule', 'List'), - '+' => array('List'), - '-' => array('SetextHeader', 'Table', 'Rule', 'List'), - '0' => array('List'), - '1' => array('List'), - '2' => array('List'), - '3' => array('List'), - '4' => array('List'), - '5' => array('List'), - '6' => array('List'), - '7' => array('List'), - '8' => array('List'), - '9' => array('List'), - ':' => array('Table'), - '<' => array('Comment', 'Markup'), - '=' => array('SetextHeader'), - '>' => array('Quote'), - '[' => array('Reference'), - '_' => array('Rule'), - '`' => array('FencedCode'), - '|' => array('Table'), - '~' => array('FencedCode'), - ); - - # ~ - - protected $unmarkedBlockTypes = array( - 'Code', - ); - - # - # Blocks - # - - protected function lines(array $lines) - { - $CurrentBlock = null; - - foreach ($lines as $line) - { - if (chop($line) === '') - { - if (isset($CurrentBlock)) - { - $CurrentBlock['interrupted'] = true; - } - - continue; - } - - if (strpos($line, "\t") !== false) - { - $parts = explode("\t", $line); - - $line = $parts[0]; - - unset($parts[0]); - - foreach ($parts as $part) - { - $shortage = 4 - mb_strlen($line, 'utf-8') % 4; - - $line .= str_repeat(' ', $shortage); - $line .= $part; - } - } - - $indent = 0; - - while (isset($line[$indent]) and $line[$indent] === ' ') - { - $indent ++; - } - - $text = $indent > 0 ? substr($line, $indent) : $line; - - # ~ - - $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); - - # ~ - - if (isset($CurrentBlock['continuable'])) - { - $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); - - if (isset($Block)) - { - $CurrentBlock = $Block; - - continue; - } - else - { - if ($this->isBlockCompletable($CurrentBlock['type'])) - { - $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); - } - } - } +// +// +// Parsedown +// http://parsedown.org +// +// (c) Emanuil Rusev +// http://erusev.com +// +// For the full license information, view the LICENSE file that was distributed +// with this source code. +// +// + +class Parsedown { + + // ~ + + const version = '1.7.2'; + + // ~ + + function text( $text ) { + // make sure no definitions are set + $this->DefinitionData = array(); + + // standardize line breaks + $text = str_replace( array( "\r\n", "\r" ), "\n", $text ); + + // remove surrounding line breaks + $text = trim( $text, "\n" ); + + // split text into lines + $lines = explode( "\n", $text ); + + // iterate through lines to identify blocks + $markup = $this->lines( $lines ); + + // trim line breaks + $markup = trim( $markup, "\n" ); + + return $markup; + } + + // + // Setters + // + + function setBreaksEnabled( $breaksEnabled ) { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped( $markupEscaped ) { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked( $urlsLinked ) { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; - # ~ + function setSafeMode( $safeMode ) { + $this->safeMode = (bool) $safeMode; - $marker = $text[0]; + return $this; + } + + protected $safeMode; - # ~ + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); - $blockTypes = $this->unmarkedBlockTypes; + // + // Lines + // - if (isset($this->BlockTypes[$marker])) - { - foreach ($this->BlockTypes[$marker] as $blockType) - { - $blockTypes []= $blockType; - } - } + protected $BlockTypes = array( + '#' => array( 'Header' ), + '*' => array( 'Rule', 'List' ), + '+' => array( 'List' ), + '-' => array( 'SetextHeader', 'Table', 'Rule', 'List' ), + '0' => array( 'List' ), + '1' => array( 'List' ), + '2' => array( 'List' ), + '3' => array( 'List' ), + '4' => array( 'List' ), + '5' => array( 'List' ), + '6' => array( 'List' ), + '7' => array( 'List' ), + '8' => array( 'List' ), + '9' => array( 'List' ), + ':' => array( 'Table' ), + '<' => array( 'Comment', 'Markup' ), + '=' => array( 'SetextHeader' ), + '>' => array( 'Quote' ), + '[' => array( 'Reference' ), + '_' => array( 'Rule' ), + '`' => array( 'FencedCode' ), + '|' => array( 'Table' ), + '~' => array( 'FencedCode' ), + ); - # - # ~ + // ~ - foreach ($blockTypes as $blockType) - { - $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); + protected $unmarkedBlockTypes = array( + 'Code', + ); - if (isset($Block)) - { - $Block['type'] = $blockType; + // + // Blocks + // - if ( ! isset($Block['identified'])) - { - $Blocks []= $CurrentBlock; + protected function lines( array $lines ) { + $CurrentBlock = null; - $Block['identified'] = true; - } + foreach ( $lines as $line ) { + if ( chop( $line ) === '' ) { + if ( isset( $CurrentBlock ) ) { + $CurrentBlock['interrupted'] = true; + } - if ($this->isBlockContinuable($blockType)) - { - $Block['continuable'] = true; - } + continue; + } - $CurrentBlock = $Block; + if ( strpos( $line, "\t" ) !== false ) { + $parts = explode( "\t", $line ); - continue 2; - } - } + $line = $parts[0]; - # ~ + unset( $parts[0] ); - if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) - { - $CurrentBlock['element']['text'] .= "\n".$text; - } - else - { - $Blocks []= $CurrentBlock; + foreach ( $parts as $part ) { + $shortage = 4 - mb_strlen( $line, 'utf-8' ) % 4; - $CurrentBlock = $this->paragraph($Line); + $line .= str_repeat( ' ', $shortage ); + $line .= $part; + } + } - $CurrentBlock['identified'] = true; - } - } + $indent = 0; - # ~ + while ( isset( $line[ $indent ] ) and $line[ $indent ] === ' ' ) { + ++$indent; + } - if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) - { - $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); - } + $text = $indent > 0 ? substr( $line, $indent ) : $line; - # ~ + // ~ - $Blocks []= $CurrentBlock; + $Line = array( + 'body' => $line, + 'indent' => $indent, + 'text' => $text, + ); - unset($Blocks[0]); + // ~ - # ~ + if ( isset( $CurrentBlock['continuable'] ) ) { + $Block = $this->{'block' . $CurrentBlock['type'] . 'Continue'}( $Line, $CurrentBlock ); - $markup = ''; + if ( isset( $Block ) ) { + $CurrentBlock = $Block; - foreach ($Blocks as $Block) - { - if (isset($Block['hidden'])) - { - continue; - } + continue; + } elseif ( $this->isBlockCompletable( $CurrentBlock['type'] ) ) { + $CurrentBlock = $this->{'block' . $CurrentBlock['type'] . 'Complete'}( $CurrentBlock ); + } + } - $markup .= "\n"; - $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); - } + // ~ - $markup .= "\n"; + $marker = $text[0]; - # ~ + // ~ - return $markup; - } + $blockTypes = $this->unmarkedBlockTypes; - protected function isBlockContinuable($Type) - { - return method_exists($this, 'block'.$Type.'Continue'); - } + if ( isset( $this->BlockTypes[ $marker ] ) ) { + foreach ( $this->BlockTypes[ $marker ] as $blockType ) { + $blockTypes [] = $blockType; + } + } - protected function isBlockCompletable($Type) - { - return method_exists($this, 'block'.$Type.'Complete'); - } + // + // ~ - # - # Code + foreach ( $blockTypes as $blockType ) { + $Block = $this->{'block' . $blockType}( $Line, $CurrentBlock ); - protected function blockCode($Line, $Block = null) - { - if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) - { - return; - } + if ( isset( $Block ) ) { + $Block['type'] = $blockType; - if ($Line['indent'] >= 4) - { - $text = substr($Line['body'], 4); + if ( ! isset( $Block['identified'] ) ) { + $Blocks [] = $CurrentBlock; - $Block = array( - 'element' => array( - 'name' => 'pre', - 'handler' => 'element', - 'text' => array( - 'name' => 'code', - 'text' => $text, - ), - ), - ); - - return $Block; - } - } - - protected function blockCodeContinue($Line, $Block) - { - if ($Line['indent'] >= 4) - { - if (isset($Block['interrupted'])) - { - $Block['element']['text']['text'] .= "\n"; - - unset($Block['interrupted']); - } - - $Block['element']['text']['text'] .= "\n"; - - $text = substr($Line['body'], 4); - - $Block['element']['text']['text'] .= $text; - - return $Block; - } - } - - protected function blockCodeComplete($Block) - { - $text = $Block['element']['text']['text']; - - $Block['element']['text']['text'] = $text; - - return $Block; - } - - # - # Comment - - protected function blockComment($Line) - { - if ($this->markupEscaped or $this->safeMode) - { - return; - } - - if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') - { - $Block = array( - 'markup' => $Line['body'], - ); - - if (preg_match('/-->$/', $Line['text'])) - { - $Block['closed'] = true; - } - - return $Block; - } - } - - protected function blockCommentContinue($Line, array $Block) - { - if (isset($Block['closed'])) - { - return; - } - - $Block['markup'] .= "\n" . $Line['body']; - - if (preg_match('/-->$/', $Line['text'])) - { - $Block['closed'] = true; - } - - return $Block; - } - - # - # Fenced Code - - protected function blockFencedCode($Line) - { - if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) - { - $Element = array( - 'name' => 'code', - 'text' => '', - ); - - if (isset($matches[1])) - { - /** - * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes - * Every HTML element may have a class attribute specified. - * The attribute, if specified, must have a value that is a set - * of space-separated tokens representing the various classes - * that the element belongs to. - * [...] - * The space characters, for the purposes of this specification, - * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), - * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and - * U+000D CARRIAGE RETURN (CR). - */ - $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r")); - - $class = 'language-'.$language; - - $Element['attributes'] = array( - 'class' => $class, - ); - } - - $Block = array( - 'char' => $Line['text'][0], - 'element' => array( - 'name' => 'pre', - 'handler' => 'element', - 'text' => $Element, - ), - ); - - return $Block; - } - } - - protected function blockFencedCodeContinue($Line, $Block) - { - if (isset($Block['complete'])) - { - return; - } - - if (isset($Block['interrupted'])) - { - $Block['element']['text']['text'] .= "\n"; - - unset($Block['interrupted']); - } - - if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) - { - $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); - - $Block['complete'] = true; - - return $Block; - } - - $Block['element']['text']['text'] .= "\n".$Line['body']; - - return $Block; - } - - protected function blockFencedCodeComplete($Block) - { - $text = $Block['element']['text']['text']; - - $Block['element']['text']['text'] = $text; - - return $Block; - } - - # - # Header - - protected function blockHeader($Line) - { - if (isset($Line['text'][1])) - { - $level = 1; - - while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') - { - $level ++; - } - - if ($level > 6) - { - return; - } - - $text = trim($Line['text'], '# '); - - $Block = array( - 'element' => array( - 'name' => 'h' . min(6, $level), - 'text' => $text, - 'handler' => 'line', - ), - ); - - return $Block; - } - } - - # - # List - - protected function blockList($Line) - { - list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); - - if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) - { - $Block = array( - 'indent' => $Line['indent'], - 'pattern' => $pattern, - 'element' => array( - 'name' => $name, - 'handler' => 'elements', - ), - ); - - if($name === 'ol') - { - $listStart = stristr($matches[0], '.', true); - - if($listStart !== '1') - { - $Block['element']['attributes'] = array('start' => $listStart); - } - } - - $Block['li'] = array( - 'name' => 'li', - 'handler' => 'li', - 'text' => array( - $matches[2], - ), - ); - - $Block['element']['text'] []= & $Block['li']; - - return $Block; - } - } - - protected function blockListContinue($Line, array $Block) - { - if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) - { - if (isset($Block['interrupted'])) - { - $Block['li']['text'] []= ''; - - $Block['loose'] = true; - - unset($Block['interrupted']); - } - - unset($Block['li']); - - $text = isset($matches[1]) ? $matches[1] : ''; - - $Block['li'] = array( - 'name' => 'li', - 'handler' => 'li', - 'text' => array( - $text, - ), - ); - - $Block['element']['text'] []= & $Block['li']; - - return $Block; - } - - if ($Line['text'][0] === '[' and $this->blockReference($Line)) - { - return $Block; - } - - if ( ! isset($Block['interrupted'])) - { - $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); - - $Block['li']['text'] []= $text; - - return $Block; - } - - if ($Line['indent'] > 0) - { - $Block['li']['text'] []= ''; - - $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); - - $Block['li']['text'] []= $text; - - unset($Block['interrupted']); - - return $Block; - } - } - - protected function blockListComplete(array $Block) - { - if (isset($Block['loose'])) - { - foreach ($Block['element']['text'] as &$li) - { - if (end($li['text']) !== '') - { - $li['text'] []= ''; - } - } - } - - return $Block; - } - - # - # Quote - - protected function blockQuote($Line) - { - if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) - { - $Block = array( - 'element' => array( - 'name' => 'blockquote', - 'handler' => 'lines', - 'text' => (array) $matches[1], - ), - ); - - return $Block; - } - } - - protected function blockQuoteContinue($Line, array $Block) - { - if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) - { - if (isset($Block['interrupted'])) - { - $Block['element']['text'] []= ''; - - unset($Block['interrupted']); - } - - $Block['element']['text'] []= $matches[1]; - - return $Block; - } - - if ( ! isset($Block['interrupted'])) - { - $Block['element']['text'] []= $Line['text']; - - return $Block; - } - } - - # - # Rule - - protected function blockRule($Line) - { - if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) - { - $Block = array( - 'element' => array( - 'name' => 'hr' - ), - ); - - return $Block; - } - } - - # - # Setext - - protected function blockSetextHeader($Line, array $Block = null) - { - if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) - { - return; - } - - if (chop($Line['text'], $Line['text'][0]) === '') - { - $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; - - return $Block; - } - } - - # - # Markup - - protected function blockMarkup($Line) - { - if ($this->markupEscaped or $this->safeMode) - { - return; - } - - if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) - { - $element = strtolower($matches[1]); - - if (in_array($element, $this->textLevelElements)) - { - return; - } - - $Block = array( - 'name' => $matches[1], - 'depth' => 0, - 'markup' => $Line['text'], - ); - - $length = strlen($matches[0]); - - $remainder = substr($Line['text'], $length); - - if (trim($remainder) === '') - { - if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) - { - $Block['closed'] = true; - - $Block['void'] = true; - } - } - else - { - if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) - { - return; - } - - if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) - { - $Block['closed'] = true; - } - } - - return $Block; - } - } - - protected function blockMarkupContinue($Line, array $Block) - { - if (isset($Block['closed'])) - { - return; - } - - if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open - { - $Block['depth'] ++; - } - - if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close - { - if ($Block['depth'] > 0) - { - $Block['depth'] --; - } - else - { - $Block['closed'] = true; - } - } - - if (isset($Block['interrupted'])) - { - $Block['markup'] .= "\n"; - - unset($Block['interrupted']); - } - - $Block['markup'] .= "\n".$Line['body']; - - return $Block; - } - - # - # Reference - - protected function blockReference($Line) - { - if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) - { - $id = strtolower($matches[1]); - - $Data = array( - 'url' => $matches[2], - 'title' => null, - ); - - if (isset($matches[3])) - { - $Data['title'] = $matches[3]; - } - - $this->DefinitionData['Reference'][$id] = $Data; - - $Block = array( - 'hidden' => true, - ); - - return $Block; - } - } - - # - # Table - - protected function blockTable($Line, array $Block = null) - { - if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) - { - return; - } - - if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') - { - $alignments = array(); - - $divider = $Line['text']; - - $divider = trim($divider); - $divider = trim($divider, '|'); - - $dividerCells = explode('|', $divider); - - foreach ($dividerCells as $dividerCell) - { - $dividerCell = trim($dividerCell); - - if ($dividerCell === '') - { - continue; - } - - $alignment = null; - - if ($dividerCell[0] === ':') - { - $alignment = 'left'; - } - - if (substr($dividerCell, - 1) === ':') - { - $alignment = $alignment === 'left' ? 'center' : 'right'; - } - - $alignments []= $alignment; - } - - # ~ - - $HeaderElements = array(); - - $header = $Block['element']['text']; - - $header = trim($header); - $header = trim($header, '|'); + $Block['identified'] = true; + } - $headerCells = explode('|', $header); + if ( $this->isBlockContinuable( $blockType ) ) { + $Block['continuable'] = true; + } - foreach ($headerCells as $index => $headerCell) - { - $headerCell = trim($headerCell); + $CurrentBlock = $Block; - $HeaderElement = array( - 'name' => 'th', - 'text' => $headerCell, - 'handler' => 'line', - ); + continue 2; + } + } - if (isset($alignments[$index])) - { - $alignment = $alignments[$index]; - - $HeaderElement['attributes'] = array( - 'style' => 'text-align: '.$alignment.';', - ); - } - - $HeaderElements []= $HeaderElement; - } - - # ~ - - $Block = array( - 'alignments' => $alignments, - 'identified' => true, - 'element' => array( - 'name' => 'table', - 'handler' => 'elements', - ), - ); - - $Block['element']['text'] []= array( - 'name' => 'thead', - 'handler' => 'elements', - ); - - $Block['element']['text'] []= array( - 'name' => 'tbody', - 'handler' => 'elements', - 'text' => array(), - ); - - $Block['element']['text'][0]['text'] []= array( - 'name' => 'tr', - 'handler' => 'elements', - 'text' => $HeaderElements, - ); - - return $Block; - } - } - - protected function blockTableContinue($Line, array $Block) - { - if (isset($Block['interrupted'])) - { - return; - } - - if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) - { - $Elements = array(); - - $row = $Line['text']; - - $row = trim($row); - $row = trim($row, '|'); - - preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); - - foreach ($matches[0] as $index => $cell) - { - $cell = trim($cell); - - $Element = array( - 'name' => 'td', - 'handler' => 'line', - 'text' => $cell, - ); - - if (isset($Block['alignments'][$index])) - { - $Element['attributes'] = array( - 'style' => 'text-align: '.$Block['alignments'][$index].';', - ); - } - - $Elements []= $Element; - } - - $Element = array( - 'name' => 'tr', - 'handler' => 'elements', - 'text' => $Elements, - ); - - $Block['element']['text'][1]['text'] []= $Element; - - return $Block; - } - } - - # - # ~ - # - - protected function paragraph($Line) - { - $Block = array( - 'element' => array( - 'name' => 'p', - 'text' => $Line['text'], - 'handler' => 'line', - ), - ); + // ~ - return $Block; - } + if ( isset( $CurrentBlock ) and ! isset( $CurrentBlock['type'] ) and ! isset( $CurrentBlock['interrupted'] ) ) { + $CurrentBlock['element']['text'] .= "\n" . $text; + } else { + $Blocks [] = $CurrentBlock; - # - # Inline Elements - # + $CurrentBlock = $this->paragraph( $Line ); - protected $InlineTypes = array( - '"' => array('SpecialCharacter'), - '!' => array('Image'), - '&' => array('SpecialCharacter'), - '*' => array('Emphasis'), - ':' => array('Url'), - '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), - '>' => array('SpecialCharacter'), - '[' => array('Link'), - '_' => array('Emphasis'), - '`' => array('Code'), - '~' => array('Strikethrough'), - '\\' => array('EscapeSequence'), - ); + $CurrentBlock['identified'] = true; + } + } - # ~ + // ~ - protected $inlineMarkerList = '!"*_&[:<>`~\\'; + if ( isset( $CurrentBlock['continuable'] ) and $this->isBlockCompletable( $CurrentBlock['type'] ) ) { + $CurrentBlock = $this->{'block' . $CurrentBlock['type'] . 'Complete'}( $CurrentBlock ); + } - # - # ~ - # + // ~ - public function line($text, $nonNestables=array()) - { - $markup = ''; + $Blocks [] = $CurrentBlock; - # $excerpt is based on the first occurrence of a marker + unset( $Blocks[0] ); - while ($excerpt = strpbrk($text, $this->inlineMarkerList)) - { - $marker = $excerpt[0]; + // ~ - $markerPosition = strpos($text, $marker); + $markup = ''; - $Excerpt = array('text' => $excerpt, 'context' => $text); + foreach ( $Blocks as $Block ) { + if ( isset( $Block['hidden'] ) ) { + continue; + } - foreach ($this->InlineTypes[$marker] as $inlineType) - { - # check to see if the current inline type is nestable in the current context + $markup .= "\n"; + $markup .= isset( $Block['markup'] ) ? $Block['markup'] : $this->element( $Block['element'] ); + } - if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables)) - { - continue; - } - - $Inline = $this->{'inline'.$inlineType}($Excerpt); - - if ( ! isset($Inline)) - { - continue; - } - - # makes sure that the inline belongs to "our" marker - - if (isset($Inline['position']) and $Inline['position'] > $markerPosition) - { - continue; - } - - # sets a default inline position - - if ( ! isset($Inline['position'])) - { - $Inline['position'] = $markerPosition; - } - - # cause the new element to 'inherit' our non nestables - - foreach ($nonNestables as $non_nestable) - { - $Inline['element']['nonNestables'][] = $non_nestable; - } - - # the text that comes before the inline - $unmarkedText = substr($text, 0, $Inline['position']); - - # compile the unmarked text - $markup .= $this->unmarkedText($unmarkedText); - - # compile the inline - $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); - - # remove the examined text - $text = substr($text, $Inline['position'] + $Inline['extent']); - - continue 2; - } - - # the marker does not belong to an inline - - $unmarkedText = substr($text, 0, $markerPosition + 1); - - $markup .= $this->unmarkedText($unmarkedText); - - $text = substr($text, $markerPosition + 1); - } - - $markup .= $this->unmarkedText($text); - - return $markup; - } - - # - # ~ - # - - protected function inlineCode($Excerpt) - { - $marker = $Excerpt['text'][0]; - - if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), - 'element' => array( - 'name' => 'code', - 'text' => $text, - ), - ); - } - } - - protected function inlineEmailTag($Excerpt) - { - if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) - { - $url = $matches[1]; - - if ( ! isset($matches[2])) - { - $url = 'mailto:' . $url; - } - - return array( - 'extent' => strlen($matches[0]), - 'element' => array( - 'name' => 'a', - 'text' => $matches[1], - 'attributes' => array( - 'href' => $url, - ), - ), - ); - } - } - - protected function inlineEmphasis($Excerpt) - { - if ( ! isset($Excerpt['text'][1])) - { - return; - } - - $marker = $Excerpt['text'][0]; - - if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) - { - $emphasis = 'strong'; - } - elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) - { - $emphasis = 'em'; - } - else - { - return; - } - - return array( - 'extent' => strlen($matches[0]), - 'element' => array( - 'name' => $emphasis, - 'handler' => 'line', - 'text' => $matches[1], - ), - ); - } - - protected function inlineEscapeSequence($Excerpt) - { - if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) - { - return array( - 'markup' => $Excerpt['text'][1], - 'extent' => 2, - ); - } - } - - protected function inlineImage($Excerpt) - { - if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') - { - return; - } - - $Excerpt['text']= substr($Excerpt['text'], 1); - - $Link = $this->inlineLink($Excerpt); - - if ($Link === null) - { - return; - } - - $Inline = array( - 'extent' => $Link['extent'] + 1, - 'element' => array( - 'name' => 'img', - 'attributes' => array( - 'src' => $Link['element']['attributes']['href'], - 'alt' => $Link['element']['text'], - ), - ), - ); - - $Inline['element']['attributes'] += $Link['element']['attributes']; - - unset($Inline['element']['attributes']['href']); - - return $Inline; - } - - protected function inlineLink($Excerpt) - { - $Element = array( - 'name' => 'a', - 'handler' => 'line', - 'nonNestables' => array('Url', 'Link'), - 'text' => null, - 'attributes' => array( - 'href' => null, - 'title' => null, - ), - ); - - $extent = 0; - - $remainder = $Excerpt['text']; - - if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) - { - $Element['text'] = $matches[1]; - - $extent += strlen($matches[0]); - - $remainder = substr($remainder, $extent); - } - else - { - return; - } - - if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) - { - $Element['attributes']['href'] = $matches[1]; - - if (isset($matches[2])) - { - $Element['attributes']['title'] = substr($matches[2], 1, - 1); - } - - $extent += strlen($matches[0]); - } - else - { - if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) - { - $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; - $definition = strtolower($definition); - - $extent += strlen($matches[0]); - } - else - { - $definition = strtolower($Element['text']); - } - - if ( ! isset($this->DefinitionData['Reference'][$definition])) - { - return; - } - - $Definition = $this->DefinitionData['Reference'][$definition]; - - $Element['attributes']['href'] = $Definition['url']; - $Element['attributes']['title'] = $Definition['title']; - } - - return array( - 'extent' => $extent, - 'element' => $Element, - ); - } - - protected function inlineMarkup($Excerpt) - { - if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) - { - return; - } - - if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) - { - return array( - 'markup' => $matches[0], - 'extent' => strlen($matches[0]), - ); - } - - if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) - { - return array( - 'markup' => $matches[0], - 'extent' => strlen($matches[0]), - ); - } - - if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) - { - return array( - 'markup' => $matches[0], - 'extent' => strlen($matches[0]), - ); - } - } - - protected function inlineSpecialCharacter($Excerpt) - { - if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) - { - return array( - 'markup' => '&', - 'extent' => 1, - ); - } - - $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); - - if (isset($SpecialCharacter[$Excerpt['text'][0]])) - { - return array( - 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', - 'extent' => 1, - ); - } - } - - protected function inlineStrikethrough($Excerpt) - { - if ( ! isset($Excerpt['text'][1])) - { - return; - } - - if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) - { - return array( - 'extent' => strlen($matches[0]), - 'element' => array( - 'name' => 'del', - 'text' => $matches[1], - 'handler' => 'line', - ), - ); - } - } - - protected function inlineUrl($Excerpt) - { - if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') - { - return; - } - - if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) - { - $url = $matches[0][0]; - - $Inline = array( - 'extent' => strlen($matches[0][0]), - 'position' => $matches[0][1], - 'element' => array( - 'name' => 'a', - 'text' => $url, - 'attributes' => array( - 'href' => $url, - ), - ), - ); - - return $Inline; - } - } - - protected function inlineUrlTag($Excerpt) - { - if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) - { - $url = $matches[1]; - - return array( - 'extent' => strlen($matches[0]), - 'element' => array( - 'name' => 'a', - 'text' => $url, - 'attributes' => array( - 'href' => $url, - ), - ), - ); - } - } - - # ~ - - protected function unmarkedText($text) - { - if ($this->breaksEnabled) - { - $text = preg_replace('/[ ]*\n/', "
\n", $text); - } - else - { - $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text); - $text = str_replace(" \n", "\n", $text); - } - - return $text; - } - - # - # Handlers - # - - protected function element(array $Element) - { - if ($this->safeMode) - { - $Element = $this->sanitiseElement($Element); - } - - $markup = '<'.$Element['name']; - - if (isset($Element['attributes'])) - { - foreach ($Element['attributes'] as $name => $value) - { - if ($value === null) - { - continue; - } - - $markup .= ' '.$name.'="'.self::escape($value).'"'; - } - } - - if (isset($Element['text'])) - { - $markup .= '>'; - - if (!isset($Element['nonNestables'])) - { - $Element['nonNestables'] = array(); - } - - if (isset($Element['handler'])) - { - $markup .= $this->{$Element['handler']}($Element['text'], $Element['nonNestables']); - } - else - { - $markup .= self::escape($Element['text'], true); - } - - $markup .= ''; - } - else - { - $markup .= ' />'; - } - - return $markup; - } - - protected function elements(array $Elements) - { - $markup = ''; - - foreach ($Elements as $Element) - { - $markup .= "\n" . $this->element($Element); - } - - $markup .= "\n"; - - return $markup; - } - - # ~ - - protected function li($lines) - { - $markup = $this->lines($lines); - - $trimmedMarkup = trim($markup); - - if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

') - { - $markup = $trimmedMarkup; - $markup = substr($markup, 3); - - $position = strpos($markup, "

"); - - $markup = substr_replace($markup, '', $position, 4); - } - - return $markup; - } - - # - # Deprecated Methods - # - - function parse($text) - { - $markup = $this->text($text); - - return $markup; - } - - protected function sanitiseElement(array $Element) - { - static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; - static $safeUrlNameToAtt = array( - 'a' => 'href', - 'img' => 'src', - ); - - if (isset($safeUrlNameToAtt[$Element['name']])) - { - $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); - } - - if ( ! empty($Element['attributes'])) - { - foreach ($Element['attributes'] as $att => $val) - { - # filter out badly parsed attribute - if ( ! preg_match($goodAttribute, $att)) - { - unset($Element['attributes'][$att]); - } - # dump onevent attribute - elseif (self::striAtStart($att, 'on')) - { - unset($Element['attributes'][$att]); - } - } - } - - return $Element; - } - - protected function filterUnsafeUrlInAttribute(array $Element, $attribute) - { - foreach ($this->safeLinksWhitelist as $scheme) - { - if (self::striAtStart($Element['attributes'][$attribute], $scheme)) - { - return $Element; - } - } - - $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); - - return $Element; - } - - # - # Static Methods - # - - protected static function escape($text, $allowQuotes = false) - { - return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); - } - - protected static function striAtStart($string, $needle) - { - $len = strlen($needle); - - if ($len > strlen($string)) - { - return false; - } - else - { - return strtolower(substr($string, 0, $len)) === strtolower($needle); - } - } - - static function instance($name = 'default') - { - if (isset(self::$instances[$name])) - { - return self::$instances[$name]; - } - - $instance = new static(); - - self::$instances[$name] = $instance; - - return $instance; - } - - private static $instances = array(); - - # - # Fields - # - - protected $DefinitionData; - - # - # Read-Only - - protected $specialCharacters = array( - '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', - ); - - protected $StrongRegex = array( - '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', - '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', - ); - - protected $EmRegex = array( - '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', - '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', - ); - - protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; - - protected $voidElements = array( - 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', - ); - - protected $textLevelElements = array( - 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', - 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', - 'i', 'rp', 'del', 'code', 'strike', 'marquee', - 'q', 'rt', 'ins', 'font', 'strong', - 's', 'tt', 'kbd', 'mark', - 'u', 'xm', 'sub', 'nobr', - 'sup', 'ruby', - 'var', 'span', - 'wbr', 'time', - ); + $markup .= "\n"; + + // ~ + + return $markup; + } + + protected function isBlockContinuable( $Type ) { + return method_exists( $this, 'block' . $Type . 'Continue' ); + } + + protected function isBlockCompletable( $Type ) { + return method_exists( $this, 'block' . $Type . 'Complete' ); + } + + // + // Code + + protected function blockCode( $Line, $Block = null ) { + if ( isset( $Block ) and ! isset( $Block['type'] ) and ! isset( $Block['interrupted'] ) ) { + return; + } + + if ( $Line['indent'] >= 4 ) { + $text = substr( $Line['body'], 4 ); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue( $Line, $Block ) { + if ( $Line['indent'] >= 4 ) { + if ( isset( $Block['interrupted'] ) ) { + $Block['element']['text']['text'] .= "\n"; + + unset( $Block['interrupted'] ); + } + + $Block['element']['text']['text'] .= "\n"; + + $text = substr( $Line['body'], 4 ); + + $Block['element']['text']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete( $Block ) { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + // + // Comment + + protected function blockComment( $Line ) { + if ( $this->markupEscaped or $this->safeMode ) { + return; + } + + if ( isset( $Line['text'][3] ) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!' ) { + $Block = array( + 'markup' => $Line['body'], + ); + + if ( preg_match( '/-->$/', $Line['text'] ) ) { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue( $Line, array $Block ) { + if ( isset( $Block['closed'] ) ) { + return; + } + + $Block['markup'] .= "\n" . $Line['body']; + + if ( preg_match( '/-->$/', $Line['text'] ) ) { + $Block['closed'] = true; + } + + return $Block; + } + + // + // Fenced Code + + protected function blockFencedCode( $Line ) { + if ( preg_match( '/^[' . $Line['text'][0] . ']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches ) ) { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ( isset( $matches[1] ) ) { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr( $matches[1], 0, strcspn( $matches[1], " \t\n\f\r" ) ); + + $class = 'language-' . $language; + + $Element['attributes'] = array( + 'class' => $class, + ); + } + + $Block = array( + 'char' => $Line['text'][0], + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => $Element, + ), + ); + + return $Block; + } + } + + protected function blockFencedCodeContinue( $Line, $Block ) { + if ( isset( $Block['complete'] ) ) { + return; + } + + if ( isset( $Block['interrupted'] ) ) { + $Block['element']['text']['text'] .= "\n"; + + unset( $Block['interrupted'] ); + } + + if ( preg_match( '/^' . $Block['char'] . '{3,}[ ]*$/', $Line['text'] ) ) { + $Block['element']['text']['text'] = substr( $Block['element']['text']['text'], 1 ); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['text']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete( $Block ) { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + // + // Header + + protected function blockHeader( $Line ) { + if ( isset( $Line['text'][1] ) ) { + $level = 1; + + while ( isset( $Line['text'][ $level ] ) and $Line['text'][ $level ] === '#' ) { + ++$level; + } + + if ( $level > 6 ) { + return; + } + + $text = trim( $Line['text'], '# ' ); + + $Block = array( + 'element' => array( + 'name' => 'h' . min( 6, $level ), + 'text' => $text, + 'handler' => 'line', + ), + ); + + return $Block; + } + } + + // + // List + + protected function blockList( $Line ) { + list($name, $pattern) = $Line['text'][0] <= '-' ? array( 'ul', '[*+-]' ) : array( 'ol', '[0-9]+[.]' ); + + if ( preg_match( '/^(' . $pattern . '[ ]+)(.*)/', $Line['text'], $matches ) ) { + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'element' => array( + 'name' => $name, + 'handler' => 'elements', + ), + ); + + if ( $name === 'ol' ) { + $listStart = stristr( $matches[0], '.', true ); + + if ( $listStart !== '1' ) { + $Block['element']['attributes'] = array( 'start' => $listStart ); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $matches[2], + ), + ); + + $Block['element']['text'] [] = & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue( $Line, array $Block ) { + if ( $Block['indent'] === $Line['indent'] and preg_match( '/^' . $Block['pattern'] . '(?:[ ]+(.*)|$)/', $Line['text'], $matches ) ) { + if ( isset( $Block['interrupted'] ) ) { + $Block['li']['text'] [] = ''; + + $Block['loose'] = true; + + unset( $Block['interrupted'] ); + } + + unset( $Block['li'] ); + + $text = isset( $matches[1] ) ? $matches[1] : ''; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $text, + ), + ); + + $Block['element']['text'] [] = & $Block['li']; + + return $Block; + } + + if ( $Line['text'][0] === '[' and $this->blockReference( $Line ) ) { + return $Block; + } + + if ( ! isset( $Block['interrupted'] ) ) { + $text = preg_replace( '/^[ ]{0,4}/', '', $Line['body'] ); + + $Block['li']['text'] [] = $text; + + return $Block; + } + + if ( $Line['indent'] > 0 ) { + $Block['li']['text'] [] = ''; + + $text = preg_replace( '/^[ ]{0,4}/', '', $Line['body'] ); + + $Block['li']['text'] [] = $text; + + unset( $Block['interrupted'] ); + + return $Block; + } + } + + protected function blockListComplete( array $Block ) { + if ( isset( $Block['loose'] ) ) { + foreach ( $Block['element']['text'] as &$li ) { + if ( end( $li['text'] ) !== '' ) { + $li['text'] [] = ''; + } + } + } + + return $Block; + } + + // + // Quote + + protected function blockQuote( $Line ) { + if ( preg_match( '/^>[ ]?(.*)/', $Line['text'], $matches ) ) { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => 'lines', + 'text' => (array) $matches[1], + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue( $Line, array $Block ) { + if ( $Line['text'][0] === '>' and preg_match( '/^>[ ]?(.*)/', $Line['text'], $matches ) ) { + if ( isset( $Block['interrupted'] ) ) { + $Block['element']['text'] [] = ''; + + unset( $Block['interrupted'] ); + } + + $Block['element']['text'] [] = $matches[1]; + + return $Block; + } + + if ( ! isset( $Block['interrupted'] ) ) { + $Block['element']['text'] [] = $Line['text']; + + return $Block; + } + } + + // + // Rule + + protected function blockRule( $Line ) { + if ( preg_match( '/^([' . $Line['text'][0] . '])([ ]*\1){2,}[ ]*$/', $Line['text'] ) ) { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + // + // Setext + + protected function blockSetextHeader( $Line, array $Block = null ) { + if ( ! isset( $Block ) or isset( $Block['type'] ) or isset( $Block['interrupted'] ) ) { + return; + } + + if ( chop( $Line['text'], $Line['text'][0] ) === '' ) { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + // + // Markup + + protected function blockMarkup( $Line ) { + if ( $this->markupEscaped or $this->safeMode ) { + return; + } + + if ( preg_match( '/^<(\w[\w-]*)(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*(\/)?>/', $Line['text'], $matches ) ) { + $element = strtolower( $matches[1] ); + + if ( in_array( $element, $this->textLevelElements ) ) { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'markup' => $Line['text'], + ); + + $length = strlen( $matches[0] ); + + $remainder = substr( $Line['text'], $length ); + + if ( trim( $remainder ) === '' ) { + if ( isset( $matches[2] ) or in_array( $matches[1], $this->voidElements ) ) { + $Block['closed'] = true; + + $Block['void'] = true; + } + } else { + if ( isset( $matches[2] ) or in_array( $matches[1], $this->voidElements ) ) { + return; + } + + if ( preg_match( '/<\/' . $matches[1] . '>[ ]*$/i', $remainder ) ) { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue( $Line, array $Block ) { + if ( isset( $Block['closed'] ) ) { + return; + } + + if ( preg_match( '/^<' . $Block['name'] . '(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*>/i', $Line['text'] ) ) { + ++$Block['depth']; + } + + if ( preg_match( '/(.*?)<\/' . $Block['name'] . '>[ ]*$/i', $Line['text'], $matches ) ) { + if ( $Block['depth'] > 0 ) { + --$Block['depth']; + } else { + $Block['closed'] = true; + } + } + + if ( isset( $Block['interrupted'] ) ) { + $Block['markup'] .= "\n"; + + unset( $Block['interrupted'] ); + } + + $Block['markup'] .= "\n" . $Line['body']; + + return $Block; + } + + // + // Reference + + protected function blockReference( $Line ) { + if ( preg_match( '/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches ) ) { + $id = strtolower( $matches[1] ); + + $Data = array( + 'url' => $matches[2], + 'title' => null, + ); + + if ( isset( $matches[3] ) ) { + $Data['title'] = $matches[3]; + } + + $this->DefinitionData['Reference'][ $id ] = $Data; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + // + // Table + + protected function blockTable( $Line, array $Block = null ) { + if ( ! isset( $Block ) or isset( $Block['type'] ) or isset( $Block['interrupted'] ) ) { + return; + } + + if ( strpos( $Block['element']['text'], '|' ) !== false and chop( $Line['text'], ' -:|' ) === '' ) { + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim( $divider ); + $divider = trim( $divider, '|' ); + + $dividerCells = explode( '|', $divider ); + + foreach ( $dividerCells as $dividerCell ) { + $dividerCell = trim( $dividerCell ); + + if ( $dividerCell === '' ) { + continue; + } + + $alignment = null; + + if ( $dividerCell[0] === ':' ) { + $alignment = 'left'; + } + + if ( substr( $dividerCell, - 1 ) === ':' ) { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments [] = $alignment; + } + + // ~ + + $HeaderElements = array(); + + $header = $Block['element']['text']; + + $header = trim( $header ); + $header = trim( $header, '|' ); + + $headerCells = explode( '|', $header ); + + foreach ( $headerCells as $index => $headerCell ) { + $headerCell = trim( $headerCell ); + + $HeaderElement = array( + 'name' => 'th', + 'text' => $headerCell, + 'handler' => 'line', + ); + + if ( isset( $alignments[ $index ] ) ) { + $alignment = $alignments[ $index ]; + + $HeaderElement['attributes'] = array( + 'style' => 'text-align: ' . $alignment . ';', + ); + } + + $HeaderElements [] = $HeaderElement; + } + + // ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'handler' => 'elements', + ), + ); + + $Block['element']['text'] [] = array( + 'name' => 'thead', + 'handler' => 'elements', + ); + + $Block['element']['text'] [] = array( + 'name' => 'tbody', + 'handler' => 'elements', + 'text' => array(), + ); + + $Block['element']['text'][0]['text'] [] = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $HeaderElements, + ); + + return $Block; + } + } + + protected function blockTableContinue( $Line, array $Block ) { + if ( isset( $Block['interrupted'] ) ) { + return; + } + + if ( $Line['text'][0] === '|' or strpos( $Line['text'], '|' ) ) { + $Elements = array(); + + $row = $Line['text']; + + $row = trim( $row ); + $row = trim( $row, '|' ); + + preg_match_all( '/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches ); + + foreach ( $matches[0] as $index => $cell ) { + $cell = trim( $cell ); + + $Element = array( + 'name' => 'td', + 'handler' => 'line', + 'text' => $cell, + ); + + if ( isset( $Block['alignments'][ $index ] ) ) { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][ $index ] . ';', + ); + } + + $Elements [] = $Element; + } + + $Element = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $Elements, + ); + + $Block['element']['text'][1]['text'] [] = $Element; + + return $Block; + } + } + + // + // ~ + // + + protected function paragraph( $Line ) { + $Block = array( + 'element' => array( + 'name' => 'p', + 'text' => $Line['text'], + 'handler' => 'line', + ), + ); + + return $Block; + } + + // + // Inline Elements + // + + protected $InlineTypes = array( + '"' => array( 'SpecialCharacter' ), + '!' => array( 'Image' ), + '&' => array( 'SpecialCharacter' ), + '*' => array( 'Emphasis' ), + ':' => array( 'Url' ), + '<' => array( 'UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter' ), + '>' => array( 'SpecialCharacter' ), + '[' => array( 'Link' ), + '_' => array( 'Emphasis' ), + '`' => array( 'Code' ), + '~' => array( 'Strikethrough' ), + '\\' => array( 'EscapeSequence' ), + ); + + // ~ + + protected $inlineMarkerList = '!"*_&[:<>`~\\'; + + // + // ~ + // + + public function line( $text, $nonNestables = array() ) { + $markup = ''; + + // $excerpt is based on the first occurrence of a marker + + while ( $excerpt = strpbrk( $text, $this->inlineMarkerList ) ) { + $marker = $excerpt[0]; + + $markerPosition = strpos( $text, $marker ); + + $Excerpt = array( + 'text' => $excerpt, + 'context' => $text, + ); + + foreach ( $this->InlineTypes[ $marker ] as $inlineType ) { + // check to see if the current inline type is nestable in the current context + + if ( ! empty( $nonNestables ) and in_array( $inlineType, $nonNestables ) ) { + continue; + } + + $Inline = $this->{'inline' . $inlineType}( $Excerpt ); + + if ( ! isset( $Inline ) ) { + continue; + } + + // makes sure that the inline belongs to "our" marker + + if ( isset( $Inline['position'] ) and $Inline['position'] > $markerPosition ) { + continue; + } + + // sets a default inline position + + if ( ! isset( $Inline['position'] ) ) { + $Inline['position'] = $markerPosition; + } + + // cause the new element to 'inherit' our non nestables + + foreach ( $nonNestables as $non_nestable ) { + $Inline['element']['nonNestables'][] = $non_nestable; + } + + // the text that comes before the inline + $unmarkedText = substr( $text, 0, $Inline['position'] ); + + // compile the unmarked text + $markup .= $this->unmarkedText( $unmarkedText ); + + // compile the inline + $markup .= isset( $Inline['markup'] ) ? $Inline['markup'] : $this->element( $Inline['element'] ); + + // remove the examined text + $text = substr( $text, $Inline['position'] + $Inline['extent'] ); + + continue 2; + } + + // the marker does not belong to an inline + + $unmarkedText = substr( $text, 0, $markerPosition + 1 ); + + $markup .= $this->unmarkedText( $unmarkedText ); + + $text = substr( $text, $markerPosition + 1 ); + } + + $markup .= $this->unmarkedText( $text ); + + return $markup; + } + + // + // ~ + // + + protected function inlineCode( $Excerpt ) { + $marker = $Excerpt['text'][0]; + + if ( preg_match( '/^(' . $marker . '+)[ ]*(.+?)[ ]*(? strlen( $matches[0] ), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag( $Excerpt ) { + if ( strpos( $Excerpt['text'], '>' ) !== false and preg_match( '/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches ) ) { + $url = $matches[1]; + + if ( ! isset( $matches[2] ) ) { + $url = 'mailto:' . $url; + } + + return array( + 'extent' => strlen( $matches[0] ), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis( $Excerpt ) { + if ( ! isset( $Excerpt['text'][1] ) ) { + return; + } + + $marker = $Excerpt['text'][0]; + + if ( $Excerpt['text'][1] === $marker and preg_match( $this->StrongRegex[ $marker ], $Excerpt['text'], $matches ) ) { + $emphasis = 'strong'; + } elseif ( preg_match( $this->EmRegex[ $marker ], $Excerpt['text'], $matches ) ) { + $emphasis = 'em'; + } else { + return; + } + + return array( + 'extent' => strlen( $matches[0] ), + 'element' => array( + 'name' => $emphasis, + 'handler' => 'line', + 'text' => $matches[1], + ), + ); + } + + protected function inlineEscapeSequence( $Excerpt ) { + if ( isset( $Excerpt['text'][1] ) and in_array( $Excerpt['text'][1], $this->specialCharacters ) ) { + return array( + 'markup' => $Excerpt['text'][1], + 'extent' => 2, + ); + } + } + + protected function inlineImage( $Excerpt ) { + if ( ! isset( $Excerpt['text'][1] ) or $Excerpt['text'][1] !== '[' ) { + return; + } + + $Excerpt['text'] = substr( $Excerpt['text'], 1 ); + + $Link = $this->inlineLink( $Excerpt ); + + if ( $Link === null ) { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset( $Inline['element']['attributes']['href'] ); + + return $Inline; + } + + protected function inlineLink( $Excerpt ) { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'nonNestables' => array( 'Url', 'Link' ), + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if ( preg_match( '/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches ) ) { + $Element['text'] = $matches[1]; + + $extent += strlen( $matches[0] ); + + $remainder = substr( $remainder, $extent ); + } else { + return; + } + + if ( preg_match( '/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches ) ) { + $Element['attributes']['href'] = $matches[1]; + + if ( isset( $matches[2] ) ) { + $Element['attributes']['title'] = substr( $matches[2], 1, - 1 ); + } + + $extent += strlen( $matches[0] ); + } else { + if ( preg_match( '/^\s*\[(.*?)\]/', $remainder, $matches ) ) { + $definition = strlen( $matches[1] ) ? $matches[1] : $Element['text']; + $definition = strtolower( $definition ); + + $extent += strlen( $matches[0] ); + } else { + $definition = strtolower( $Element['text'] ); + } + + if ( ! isset( $this->DefinitionData['Reference'][ $definition ] ) ) { + return; + } + + $Definition = $this->DefinitionData['Reference'][ $definition ]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup( $Excerpt ) { + if ( $this->markupEscaped or $this->safeMode or strpos( $Excerpt['text'], '>' ) === false ) { + return; + } + + if ( $Excerpt['text'][1] === '/' and preg_match( '/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches ) ) { + return array( + 'markup' => $matches[0], + 'extent' => strlen( $matches[0] ), + ); + } + + if ( $Excerpt['text'][1] === '!' and preg_match( '/^/s', $Excerpt['text'], $matches ) ) { + return array( + 'markup' => $matches[0], + 'extent' => strlen( $matches[0] ), + ); + } + + if ( $Excerpt['text'][1] !== ' ' and preg_match( '/^<\w[\w-]*(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*\/?>/s', $Excerpt['text'], $matches ) ) { + return array( + 'markup' => $matches[0], + 'extent' => strlen( $matches[0] ), + ); + } + } + + protected function inlineSpecialCharacter( $Excerpt ) { + if ( $Excerpt['text'][0] === '&' and ! preg_match( '/^&#?\w+;/', $Excerpt['text'] ) ) { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array( + '>' => 'gt', + '<' => 'lt', + '"' => 'quot', + ); + + if ( isset( $SpecialCharacter[ $Excerpt['text'][0] ] ) ) { + return array( + 'markup' => '&' . $SpecialCharacter[ $Excerpt['text'][0] ] . ';', + 'extent' => 1, + ); + } + } + + protected function inlineStrikethrough( $Excerpt ) { + if ( ! isset( $Excerpt['text'][1] ) ) { + return; + } + + if ( $Excerpt['text'][1] === '~' and preg_match( '/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches ) ) { + return array( + 'extent' => strlen( $matches[0] ), + 'element' => array( + 'name' => 'del', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl( $Excerpt ) { + if ( $this->urlsLinked !== true or ! isset( $Excerpt['text'][2] ) or $Excerpt['text'][2] !== '/' ) { + return; + } + + if ( preg_match( '/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE ) ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen( $matches[0][0] ), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag( $Excerpt ) { + if ( strpos( $Excerpt['text'], '>' ) !== false and preg_match( '/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches ) ) { + $url = $matches[1]; + + return array( + 'extent' => strlen( $matches[0] ), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + // ~ + + protected function unmarkedText( $text ) { + if ( $this->breaksEnabled ) { + $text = preg_replace( '/[ ]*\n/', "
\n", $text ); + } else { + $text = preg_replace( '/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text ); + $text = str_replace( " \n", "\n", $text ); + } + + return $text; + } + + // + // Handlers + // + + protected function element( array $Element ) { + if ( $this->safeMode ) { + $Element = $this->sanitiseElement( $Element ); + } + + $markup = '<' . $Element['name']; + + if ( isset( $Element['attributes'] ) ) { + foreach ( $Element['attributes'] as $name => $value ) { + if ( $value === null ) { + continue; + } + + $markup .= ' ' . $name . '="' . self::escape( $value ) . '"'; + } + } + + if ( isset( $Element['text'] ) ) { + $markup .= '>'; + + if ( ! isset( $Element['nonNestables'] ) ) { + $Element['nonNestables'] = array(); + } + + if ( isset( $Element['handler'] ) ) { + $markup .= $this->{$Element['handler']}( $Element['text'], $Element['nonNestables'] ); + } else { + $markup .= self::escape( $Element['text'], true ); + } + + $markup .= ''; + } else { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements( array $Elements ) { + $markup = ''; + + foreach ( $Elements as $Element ) { + $markup .= "\n" . $this->element( $Element ); + } + + $markup .= "\n"; + + return $markup; + } + + // ~ + + protected function li( $lines ) { + $markup = $this->lines( $lines ); + + $trimmedMarkup = trim( $markup ); + + if ( ! in_array( '', $lines ) and substr( $trimmedMarkup, 0, 3 ) === '

' ) { + $markup = $trimmedMarkup; + $markup = substr( $markup, 3 ); + + $position = strpos( $markup, '

' ); + + $markup = substr_replace( $markup, '', $position, 4 ); + } + + return $markup; + } + + // + // Deprecated Methods + // + + function parse( $text ) { + $markup = $this->text( $text ); + + return $markup; + } + + protected function sanitiseElement( array $Element ) { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if ( isset( $safeUrlNameToAtt[ $Element['name'] ] ) ) { + $Element = $this->filterUnsafeUrlInAttribute( $Element, $safeUrlNameToAtt[ $Element['name'] ] ); + } + + if ( ! empty( $Element['attributes'] ) ) { + foreach ( $Element['attributes'] as $att => $val ) { + // filter out badly parsed attribute + if ( ! preg_match( $goodAttribute, $att ) ) { + unset( $Element['attributes'][ $att ] ); + } + // dump onevent attribute + elseif ( self::striAtStart( $att, 'on' ) ) { + unset( $Element['attributes'][ $att ] ); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute( array $Element, $attribute ) { + foreach ( $this->safeLinksWhitelist as $scheme ) { + if ( self::striAtStart( $Element['attributes'][ $attribute ], $scheme ) ) { + return $Element; + } + } + + $Element['attributes'][ $attribute ] = str_replace( ':', '%3A', $Element['attributes'][ $attribute ] ); + + return $Element; + } + + // + // Static Methods + // + + protected static function escape( $text, $allowQuotes = false ) { + return htmlspecialchars( $text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8' ); + } + + protected static function striAtStart( $string, $needle ) { + $len = strlen( $needle ); + + if ( $len > strlen( $string ) ) { + return false; + } else { + return strtolower( substr( $string, 0, $len ) ) === strtolower( $needle ); + } + } + + static function instance( $name = 'default' ) { + if ( isset( self::$instances[ $name ] ) ) { + return self::$instances[ $name ]; + } + + $instance = new static(); + + self::$instances[ $name ] = $instance; + + return $instance; + } + + private static $instances = array(); + + // + // Fields + // + + protected $DefinitionData; + + // + // Read-Only + + protected $specialCharacters = array( + '\\', + '`', + '*', + '_', + '{', + '}', + '[', + ']', + '(', + ')', + '>', + '#', + '+', + '-', + '.', + '!', + '|', + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + + protected $voidElements = array( + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'param', + 'source', + ); + + protected $textLevelElements = array( + 'a', + 'br', + 'bdo', + 'abbr', + 'blink', + 'nextid', + 'acronym', + 'basefont', + 'b', + 'em', + 'big', + 'cite', + 'small', + 'spacer', + 'listing', + 'i', + 'rp', + 'del', + 'code', + 'strike', + 'marquee', + 'q', + 'rt', + 'ins', + 'font', + 'strong', + 's', + 'tt', + 'kbd', + 'mark', + 'u', + 'xm', + 'sub', + 'nobr', + 'sup', + 'ruby', + 'var', + 'span', + 'wbr', + 'time', + ); } diff --git a/phpcs.xml b/phpcs.xml index eb0212e..aa68a3a 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -8,9 +8,11 @@ assets/* */node_modules/* */vendor/* - */tests/ Gruntfile.js + + + @@ -21,8 +23,8 @@ - - + + @@ -56,6 +58,13 @@ 0 + + + + + + + *\.php$ tests/ @@ -68,4 +77,4 @@ 0 - + \ No newline at end of file