diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..b0707f5 --- /dev/null +++ b/.distignore @@ -0,0 +1,56 @@ +# Export ignore handling + +# Directories +/.git +/.github +/.wordpress-org +/bin +/dev +/docs +/github +/tests +/workspace + +# Dot-files +.babelrc +.distignore +.editorconfig +.env +.env.example +.env.dist +.env.docker +.env.testing.slic +.eslintignore +.eslintrc.json +.gitattributes +.gitignore +.jshintrc +.nvmrc +.phpstorm.meta.php +.scrutinizer.yml +.travis.yml + +# Other files +babel.config.js +CODE_OF_CONDUCT.md +codeception.dist.yml +codeception.example.yml +codeception.slic.yml +CODEOWNERS +composer.json +CONTRIBUTING.md +Gruntfile.js +jest-setup-wordpress-globals.js +jest.config.json +jest.config.js +package-lock.json +package.json +phpcs.xml +phpcs.xml.dist +phpunit.xml.dist +README.md +rollup.config.js +TESTS.md +webpack.common.js +webpack.dev.js +webpack.prod.js diff --git a/.gitattributes b/.gitattributes index f737362..7fcd48d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,23 @@ # Auto detect text files and perform LF normalization * text=auto +*.css linguist-generated=true +*.min.css linguist-generated=true +*.min.js linguist-generated=true +*.snap linguist-generated=true +tests/_support/_generated/* linguist-generated=true + +# Export ignore handling + +# Directories +/.git export-ignore +/.github export-ignore +/.wordpress-org export-ignore +/bin export-ignore +/dev export-ignore +/docs export-ignore +/tests export-ignore + # Ignore other files /.env.* export-ignore /codeception.* export-ignore @@ -8,26 +25,49 @@ /docs export-ignore /phpcs.xml export-ignore /README.md export-ignore -/tests export-ignore /TESTS.md export-ignore /vendor export-ignore -# Custom for Visual Studio -*.cs diff=csharp -*.sln merge=union -*.csproj merge=union -*.vbproj merge=union -*.fsproj merge=union -*.dbproj merge=union +# Dot-files +.babelrc export-ignore +.distignore export-ignore +.editorconfig export-ignore +.env export-ignore +.env.example export-ignore +.env.dist export-ignore +.env.docker export-ignore +.env.testing.slic export-ignore +.eslintignore export-ignore +.eslintrc.json export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.jshintrc export-ignore +.nvmrc export-ignore +.phpstorm.meta.php export-ignore +.scrutinizer.yml export-ignore +.travis.yml export-ignore -# Standard to msysgit -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain +# Other files +babel.config.js export-ignore +CODE_OF_CONDUCT.md export-ignore +codeception.dist.yml export-ignore +codeception.example.yml export-ignore +codeception.slic.yml export-ignore +CODEOWNERS export-ignore +composer.json export-ignore +CONTRIBUTING.md export-ignore +Gruntfile.js export-ignore +jest-setup-wordpress-globals.js export-ignore +jest.config.json export-ignore +jest.config.js export-ignore +package-lock.json export-ignore +package.json export-ignore +phpcs.xml export-ignore +phpcs.xml.dist export-ignore +phpunit.xml.dist export-ignore +README.md export-ignore +rollup.config.js export-ignore +TESTS.md export-ignore +webpack.common.js export-ignore +webpack.dev.js export-ignore +webpack.prod.js export-ignore diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 100ac84..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,67 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main, release/* ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main, release/* ] - schedule: - - cron: '41 15 * * 4' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml deleted file mode 100644 index 2efd2a5..0000000 --- a/.github/workflows/phpcs.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: 'PHP CodeSniffer' -on: - pull_request: - paths: - - 'includes/**.php' - - '*.php' -jobs: - phpcs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Get Composer Cache Directory - id: composer-cache - run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v1 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- - - name: Run PHPCS inspection - uses: rtCamp/action-phpcs-code-review@v2 - env: - GH_BOT_TOKEN: ${{ secrets.GH_BOT_TOKEN }} - SKIP_FOLDERS: "tests,.github,vendor" diff --git a/.github/workflows/wordpress-plugin-deploy.yml b/.github/workflows/wordpress-plugin-deploy.yml index 8a8afae..8b6cfea 100644 --- a/.github/workflows/wordpress-plugin-deploy.yml +++ b/.github/workflows/wordpress-plugin-deploy.yml @@ -13,15 +13,8 @@ jobs: - name: Checkout the code uses: actions/checkout@v4 - name: WordPress Plugin Deploy - uses: 10up/action-wordpress-plugin-deploy@master + uses: 10up/action-wordpress-plugin-deploy@develop env: SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} SVN_USERNAME: ${{ secrets.SVN_USERNAME }} VERSION: ${{ env.PLUGIN_VERSION }} - - name: Send message to Slack API - uses: archive/github-actions-slack@v2.7.0 - id: notify - with: - slack-bot-user-oauth-access-token: ${{ secrets.SLACK_BOT_USER_OAUTH_ACCESS_TOKEN }} - slack-channel: C02SWND14 - slack-text: The ${{ github.event.repository.name }} tag has been deployed to svn.wordpress.org 🎉 diff --git a/.github/workflows/wordpress-version-checker.yml b/.github/workflows/wordpress-version-checker.yml new file mode 100644 index 0000000..169065b --- /dev/null +++ b/.github/workflows/wordpress-version-checker.yml @@ -0,0 +1,20 @@ +name: "WordPress version checker" +on: + push: + branches: + - main + - release/* + schedule: + - cron: '0 0 * * *' + +permissions: + issues: write + +jobs: + wordpress-version-checker: + runs-on: ubuntu-latest + steps: + - name: WordPress version checker + uses: skaut/wordpress-version-checker@v1.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/wporg-update-versions.yml b/.github/workflows/wporg-update-versions.yml index 1810e1a..9123734 100644 --- a/.github/workflows/wporg-update-versions.yml +++ b/.github/workflows/wporg-update-versions.yml @@ -17,9 +17,6 @@ on: minimum_php_version: description: 'Minimum PHP version' required: false - minimum_mysql_version: - description: 'Minimum MySQL version' - required: false pull_request: paths: - 'package.json' diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..d0abedf --- /dev/null +++ b/changelog.txt @@ -0,0 +1,137 @@ +Found a bug? Have a great feature idea? Get on GitHub and tell us about it and we'll get right on it: https://github.com/pods-framework/pods-gravity-forms/issues/new + +Our GitHub has the full list of all prior releases of Pods Gravity Forms: https://github.com/pods-framework/pods-gravity-forms/releases + += 1.5.0 - March 29th, 2024 = + +* New requirements that match Pods: WP 6.0+, PHP 7.2+, and Pods 3.0+ to prep for Pods Gravity Forms 2.0 (@sc0ttkclark) +* Added: Support value overrides for checkbox GF fields when prepopulating. (@sc0ttkclark) +* Added: Allow for passing `?pods_gf_debug=1` to a form submit URL to debug the form submission mapping to Pods which outputs debug information and stops before the save runs). (@sc0ttkclark) +* Added: New hook `pods_gf_dynamic_select_show_empty_option` lets you disable showing the "empty option" in dynamic fields being prepopulated. (@sc0ttkclark) +* Added: New hook `pods_gf_addon_options_{$form_id}` that lets you filter the options built for a feed for the `Pods_GF` object. (@sc0ttkclark) +* Tweak: New `pods-gf-ui-view-only` class added to the view-only mode. (@sc0ttkclark) +* Tweak: Expanded secondary submit handling with the ability to have cancel button. (@sc0ttkclark) +* Fixed: Resolved various PHP notices. (@sc0ttkclark) +* Fixed: Removed "comments" from the field mapping options in the feed. (@sc0ttkclark) +* Fixed: Remove extra HTML in the feed labels. (@sc0ttkclark) +* Fixed: For non-select GF field types, trim the dashes on the custom `select_text` option used. (@sc0ttkclark) +* Fixed: Prepopulating field values works more consistently now when passing the prepopulated filter pre-chunked arrays of values. (@sc0ttkclark) +* Fixed: View-only mode forced to use page zero. (@sc0ttkclark) +* Fixed: Prevent duplicate submissions during feed processing. (@sc0ttkclark) +* Fixed: Only allow working with active leads in `Pods_GF_UI`. (@sc0ttkclark) +* Fixed: Prevent Markdown conflicts with other plugins and update PHP 8 compatibility by switching to the Parsedown library. #166 (@sc0ttkclark) +* Fixed: Stop prepopulating fields that aren't opted-in to it. #168 (@sc0ttkclark) + +ALSO: Pods Gravity Forms 2.0 is still in development and it brings complete compatibility with the latest Gravity Forms releases. We could use your support to help it get over the finish line this year. Please consider [donating to the Pods project](https://friends.pods.io/) to help us get there more quickly. + += 1.4.5 - July 22nd, 2022 = + +* Tested against WP 6.0 +* Added: Not seeing something map correctly? As a site admin, you now have the power to debug the form submission and see what might be going on. Add `?pods_gf_debug_gf_to_pods=1` to the URL of the form action before submitting to take advantage of the admin-only debug mode. This will output the values as they would be sent to Pods, the entry information used to reference it, and the feed options used at the time. It will stop the form from completely saving to Pods so you can tweak and debug your form feeds however much you'd like to perfect them. (@sc0ttkclark) +* Fixed: Conditional checks for feeds has been resolved and now won't get confused when there are multiple feeds for the same form in certain cases. (@sc0ttkclark) +* Fixed: Additional compatibility with Gravity Flow. #157 (@JoryHogeveen) +* Pods 2.9 is in beta and after it is released, new mapping of GF List Fields to Pods repeatable fields will be added. This add-on will also be updated with minimum version requirements updated for WP 5.5+, Pods 2.8+, and Gravity Forms 2.5+. Complete testing will be done at that time to ensure complete compatibility. + += 1.4.4 - October 6th, 2021 = + +* Tested against WP 5.8 +* Get ready for Pods 2.8 in just a week! This add-on will receive updates to ensure it is compatible with the latest Gravity Forms and the changes in Pods 2.8 + += 1.4.3 - March 26th, 2020 = + +* Added: Now requiring PHP 5.4+ +* Added: Freemius support when running Pods 2.7.17 +* Fixed: Prepopulate handling for relationship fields. +* Fixed: Prevent errors when form doesn't exist by the time it gets to our hook. + += 1.4.2 - March 2nd, 2020 = + +* Fixed: Ajax handling for various callbacks that hook into `gform_pre_render`. +* Fixed: Cleaned up logic and prevent PHP notices with multi-select arrays when setting up choices arrays. +* Fixed: Make sure `Pods_GF_UI` does not return false on UI callbacks to prevent access errors. +* Fixed: Add mapping feeds to the import/export! (props @travislopes) + += 1.4.1 - October 16th, 2018 = + +* Fixed: When syncing multiple entries, the field values were caching and not unique per entry resulting in what appeared to be duplicated content inserts/updates. + += 1.4 - October 16th, 2018 = + +* Support: Added support for Gravity Forms 2.3 database tables changes (You may see a warning on the Edit Pod screen but this is a false positive because we cache a list of all tables to transients and it triggers the warning solved by removing those old "rg" tables) +* Changed: Backwards compatibility issue -- You can now more easily set custom override values, however the old style was not able to be brought over -- you'll want to update your feeds when possible, the old values will not show up and you'll have to select the custom override value option once more, then fill it in +* Changed: Backwards compatibility issue -- Now requiring WordPress 4.6+ +* Feature: When editing entries in the admin area, changes now sync to the associated Pod item (except trash/deletes) +* Feature: New Bulk Entry Syncing to Pods WP-CLI command `wp pods-gf sync --form=123` or you can specify which feed (even if it is not active) with `wp pods-gf sync --form=123 --feed=2` +* Feature: Support for List field mapping to a Pod field which ends up serializing the value, but can be prepopulated back into the Gravity Form +* Feature: List field mapping to relationship fields related to another Pod (list columns map to individual fields in the related Pod) with new filters `pods_gf_field_columns_mapping` and `pods_gf_field_column_row` +* Feature: Support for Chained Select field mapping to a Pod field +* Feature: New Custom fields section added for Pods that support meta (Posts, Terms, Users, Media, and Comments), you can set additional custom fields including ability to set custom values there too +* Feature: Ability to set conditional processing per feed, based on specific values submitted +* Added: Whenever you create a new feed, mapping will automatically be associated between a Gravity Form field and a Pod field if the labels match +* Added: Custom override values now support GF merge tags by default (no insert UI yet) like `{form_id}` and any other merge tag +* Added: Required WP Object Fields in mapping are no longer required if you choose to 'Enable editing with this form using ____' option for Post/Media or User pod types +* Added: Support for E-mail field mappings with 'Confirm E-mail' enabled +* Added: Support for Date fields with multiple inputs (date dropdown / text fields) +* Added: Smarter requirement handling for WP object fields based on object type (only require what the WP insert API requires) +* Added: New mapping fields are now available for more Entry and Payment fields +* Added: New merge tags `{pods.id}` and `{pods.permalink}` are available for usage and in the merge tag selection dropdowns +* Improved: Added headings to each group of feed options so they are easier to work with +* Improved: Address field mapping for Country, State, and CA Provinces now convert properly to their Pods counterparts +* Updated: PHP Markdown library updated to 1.0.2 +* Fixed: Issues with using 'bypass' as a save action +* Fixed: Dynamic select options should set the current value (as posted in form) properly +* Fixed: Date/time fields shouldn't auto populate with empty dates such as 0000-00-00 anymore +* Fixed: Additional attachment processing fixes +* Fixed: Lots of Pods GF UI issues resolved +* Fixed: Removed Autocomplete limit (was 30) that was being enforced, now all data from related field will show +* Fixed: Dynamic mapping value checking to support arrays of values +* Fixed: Lots of Prepopulating fixes +* Fixed: Now supports multi page form validation and prepopulating + += 1.3 - June 2nd, 2017 = + +* Added: When creating new feeds mapping will automatically be detected based on matching field labels +* Added: New option to prepopulate the form fields with data based on the field mapping in the feed (same type of logic as edit). Limitations with certain field types, please submit issues with problems you find here. +* Added: Rewrote the whole File Upload field mapping logic and tested against Single/Multi file fields (props @mika31, @copperleaf, @zanematthew, @zorog, @chriswagoner for testing help, props @spivurno for official GF support code help) +* Added: Support for feeds with submissions from the forms embedded on the dashboard and in the admin area (props @richardW8k) +* Added: Field names to field mapping screen +* Added: Ability to define a custom override value for each field mapping +* Added: Ability to enable editing of user data using current logged in user ID (only for User pod feeds) +* Added: Ability to enable editing of post data using current post ID on singular templates (only for Post type pod feeds) +* Added: Ability to define custom 'content' in Pods GF UI custom actions instead of including a form +* Added: Ability to relate to GF forms using a relationship field (new option: Gravity Forms > Forms) +* Added: Ability to map Address and List fields +* Added: Ability to map Category and Post Tag fields +* Added: Ability to map sub fields to a pod field (Name [First Name], Address [Street Line 1], etc) +* Fixed: Ensure time fields get mapped correctly (props @mmarvin1) +* Fixed: Ensure default pods-gf-ui shortcode is only added/run on content within the loop (props @jamesgol) +* Fixed: Empty id used for Pods GF UI +* Fixed: Callback handling for Pods GF UI +* Fixed: Default Post Author mapping + += 1.2 - October 4th, 2016 = + +* Added: When using a custom action and setting the form ID option in Pods GF UI, a new custom action will be used which embeds the GF form (if no callback provided in action_data option) +* Added: New Pods GF UI option, specific to each action, for `action_link` which corresponds to the `action_links` Pods UI option +* Fixed: Support for recent GF versions where pre_save_id hook uses a different Form-specific naming convention +* Fixed: Custom confirmation handling may have not been functioning properly in some cases +* Fixed: Removed some issues that were causing PHP notices + + += 1.1 - June 13th, 2016 = + +* Added: Support for edit mode when using the Pods GF add-on mapping in the GF UI -- Use the new filter `pods_gf_addon_edit_id`, just return the ID to edit and the options will automatically be set for you +* Added: When filtering the Pods data in `Pods_GF::gf_to_pods()` (via the `pods_gf_to_pods_data` and related filters), if you set the proper ID field in that array it will now be used to *save* over the existing item; Helpful for dynamic editing configurations based upon different processes and workflows in the code +* Added: `Pods_GF::confirmation()` now supports `{@gf_to_pods_id}` replacement in confirmation URLs, replacing the variable properly to the resulting saved ID +* Fixed: `Pods_GF::_gf_to_pods_handler()` would sometimes get the action improperly set to `edit`, but only `add`, `save`, or `bypass` are valid +* Fixed: When an invalid pod is called in `Pods_GF::_gf_to_pods_handler()`, there's now a proper fallback to avoid PHP errors/warnings/notices +* Fixed: When an invalid pod is called in `Pods_GF::_gf_field_validation()`, there's now a proper fallback to avoid PHP errors/warnings/notices +* Fixed: `Pods_GF::confirmation()` would add the `gform_confirmation_{$form_id}` filter incorrectly and would cause PHP warnings about the callback, causing the confirmation functionality to not work properly +* Fixed: `Pods_GF::confirmation()` confirmation URL replacement now handles a few more cases where previously PHP notices would result +* Changed: `Pods_GF` is now storing multiple instances statically, cannot be called with `new Pods_GF()`, must be called with `Pods_GF::get_instance()` but more importantly should be called through the standard `pods_gf()` helper function to remain backwards compatible with previous versions +* Changed: `Pods_GF::$gf_to_pods_id` is no longer an integer, but an array of integers keyed by the GF Form ID +* Changed: `Pods_GF::$keep_files` is no longer an boolean, but an array of booleans keyed by the GF Form ID + += 1.0 - March 4th, 2016 = + +* Initial release diff --git a/composer.json b/composer.json index 54ce1dc..e73b59a 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "gravity-forms" ], "homepage": "https://github.com/pods-framework/pods-gravity-forms", - "license": "GPL-2.0+", + "license": "GPL-2.0-or-later", "authors": [ { "name": "Pods Framework Team", @@ -31,6 +31,7 @@ ], "support": { "issues": "https://github.com/pods-framework/pods-gravity-forms/issues", + "forum": "https://wordpress.org/support/plugin/pods-gravity-forms", "source": "https://github.com/pods-framework/pods-gravity-forms" }, "autoload": { @@ -40,11 +41,40 @@ } }, "require": { - "composer/installers": "~1.0", - "php": ">=5.4" + "composer/installers": "2.2.*", + "erusev/parsedown": "^1.7.4", + "php": ">=7.2" }, "require-dev": { - "lucatume/wp-browser": "^3.0", - "vlucas/phpdotenv": "^4.2" + "brianhenryie/strauss": "*" + }, + "scripts": { + "prefix-namespaces": [ + "strauss" + ], + "post-install-cmd": [ + "@prefix-namespaces" + ], + "post-update-cmd": [ + "@prefix-namespaces" + ] + }, + "extra": { + "installer-name": "pods-gravity-forms", + "strauss": { + "target_directory": "vendor-prefixed", + "namespace_prefix": "Pods_Gravity_Forms\\Prefixed\\", + "classmap_prefix": "Pods_Gravity_Forms__Prefixed__", + "constant_prefix": "PODS_GRAVITY_FORMS_PREFIXED_", + "function_prefix": "pods_gravity_forms_prefixed_", + "packages": [ + "erusev/parsedown" + ] + } + }, + "config": { + "allow-plugins": { + "composer/installers": true + } } -} \ No newline at end of file +} diff --git a/includes/Markdown.php b/includes/Markdown.php deleted file mode 100644 index 05a22da..0000000 --- a/includes/Markdown.php +++ /dev/null @@ -1,2109 +0,0 @@ - - // - // Original Markdown - // Copyright (c) 2004-2006 John Gruber - // - // - define( 'MARKDOWN_VERSION', '1.0.2' ); - // 29 Nov 2013 - // - // Global default settings: - // - // Change to ">" for HTML output - @define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', ' />' ); - - // Define the width of a tab for code blocks. - @define( 'MARKDOWN_TAB_WIDTH', 4 ); - - // - // WordPress settings: - // - // Change to false to remove Markdown from posts and/or comments. - @define( 'MARKDOWN_WP_POSTS', true ); - @define( 'MARKDOWN_WP_COMMENTS', true ); - - // Standard Function Interface ### - @define( 'MARKDOWN_PARSER_CLASS', 'Markdown_Parser' ); - - /** - * @param $text - * - * @return mixed - */ - function Markdown( $text ) { - - // - // Initialize the parser and return the result of its transform method. - // - // Setup static parser variable. - static $parser; - if ( ! isset( $parser ) ) { - $parser_class = MARKDOWN_PARSER_CLASS; - $parser = new $parser_class(); - } - - // Transform text using parser. - return $parser->transform( $text ); - } - - // WordPress Plugin Interface ### - if ( isset( $wp_version ) ) { - // More details about how it works here: - // - // Post content and excerpts - // - Remove WordPress paragraph generator. - // - Run Markdown on excerpt, then remove all tags. - // - Add paragraph tag around the excerpt, but remove it for the excerpt rss. - if ( MARKDOWN_WP_POSTS ) { - remove_filter( 'the_content', 'wpautop' ); - remove_filter( 'the_content_rss', 'wpautop' ); - remove_filter( 'the_excerpt', 'wpautop' ); - add_filter( 'the_content', 'Markdown', 6 ); - add_filter( 'the_content_rss', 'Markdown', 6 ); - add_filter( 'get_the_excerpt', 'Markdown', 6 ); - add_filter( 'get_the_excerpt', 'trim', 7 ); - add_filter( 'the_excerpt', 'mdwp_add_p' ); - add_filter( 'the_excerpt_rss', 'mdwp_strip_p' ); - - remove_filter( 'content_save_pre', 'balanceTags', 50 ); - remove_filter( 'excerpt_save_pre', 'balanceTags', 50 ); - add_filter( 'the_content', 'balanceTags', 50 ); - add_filter( 'get_the_excerpt', 'balanceTags', 9 ); - } - - // Comments - // - Remove WordPress paragraph generator. - // - Remove WordPress auto-link generator. - // - Scramble important tags before passing them to the kses filter. - // - Run Markdown on excerpt then remove paragraph tags. - if ( MARKDOWN_WP_COMMENTS ) { - remove_filter( 'comment_text', 'wpautop', 30 ); - remove_filter( 'comment_text', 'make_clickable' ); - add_filter( 'pre_comment_content', 'Markdown', 6 ); - add_filter( 'pre_comment_content', 'mdwp_hide_tags', 8 ); - add_filter( 'pre_comment_content', 'mdwp_show_tags', 12 ); - add_filter( 'get_comment_text', 'Markdown', 6 ); - add_filter( 'get_comment_excerpt', 'Markdown', 6 ); - add_filter( 'get_comment_excerpt', 'mdwp_strip_p', 7 ); - - global $mdwp_hidden_tags, $mdwp_placeholders; - $mdwp_hidden_tags = explode( ' ', '

 
  • ' ); - $mdwp_placeholders = explode( ' ', str_rot13( 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR ' . 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli' ) ); - } - - /** - * @param $text - * - * @return mixed|string - */ - function mdwp_add_p( $text ) { - - if ( ! preg_match( '{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text ) ) { - $text = '

    ' . $text . '

    '; - $text = preg_replace( '{\n{2,}}', "

    \n\n

    ", $text ); - } - - return $text; - } - - /** - * @param $t - * - * @return mixed - */ - function mdwp_strip_p( $t ) { - - return preg_replace( '{}i', '', $t ); - } - - /** - * @param $text - * - * @return mixed - */ - function mdwp_hide_tags( $text ) { - - global $mdwp_hidden_tags, $mdwp_placeholders; - - return str_replace( $mdwp_hidden_tags, $mdwp_placeholders, $text ); - } - - /** - * @param $text - * - * @return mixed - */ - function mdwp_show_tags( $text ) { - - global $mdwp_hidden_tags, $mdwp_placeholders; - - return str_replace( $mdwp_placeholders, $mdwp_hidden_tags, $text ); - } - }//end if - - // bBlog Plugin Info ### - /** - * @return array - */ - function identify_modifier_markdown() { - - return array( - 'name' => 'markdown', - 'type' => 'modifier', - 'nicename' => 'Markdown', - 'description' => 'A text-to-HTML conversion tool for web writers', - 'authors' => 'Michel Fortin and John Gruber', - 'licence' => 'BSD-like', - 'version' => MARKDOWN_VERSION, - 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...', - ); - } - - // Smarty Modifier Interface ### - /** - * @param $text - * - * @return mixed - */ - function smarty_modifier_markdown( $text ) { - - return Markdown( $text ); - } - - // Textile Compatibility Mode ### - // Rename this file to "classTextile.php" and it can replace Textile everywhere. - if ( strcasecmp( substr( __FILE__, - 16 ), 'classTextile.php' ) == 0 ) { - // Try to include PHP SmartyPants. Should be in the same directory. - @include_once 'smartypants.php'; - - // Fake Textile class. It calls Markdown instead. - - /** - * Class Textile - */ - class Textile { - - /** - * @param $text - * @param string $lite - * @param string $encode - * - * @return mixed - */ - public function TextileThis( $text, $lite = '', $encode = '' ) { - - if ( $lite == '' && $encode == '' ) { - $text = Markdown( $text ); - } - if ( function_exists( 'SmartyPants' ) ) { - $text = SmartyPants( $text ); - } - - return $text; - } - - // Fake restricted version: restrictions are not supported for now. - - /** - * @param $text - * @param string $lite - * @param string $noimage - * - * @return mixed - */ - public function TextileRestricted( $text, $lite = '', $noimage = '' ) { - - return $this->TextileThis( $text, $lite ); - } - - // Workaround to ensure compatibility with TextPattern 4.0.3. - - /** - * @param $text - * - * @return mixed - */ - public function blockLite( $text ) { - - return $text; - } - } - }//end if - - // - // Markdown Parser Class - // - - /** - * Class Markdown_Parser - */ - class Markdown_Parser { - - // Configuration Variables ### - // Change to ">" for HTML output. - public $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; - public $tab_width = MARKDOWN_TAB_WIDTH; - - // Change to `true` to disallow markup or entities. - public $no_markup = false; - public $no_entities = false; - - // Predefined urls and titles for reference links and images. - public $predef_urls = array(); - public $predef_titles = array(); - - // Parser Implementation ### - // Regex to match balanced [brackets]. - // Needed to insert a maximum bracked depth while converting to PHP. - public $nested_brackets_depth = 6; - public $nested_brackets_re; - - public $nested_url_parenthesis_depth = 4; - public $nested_url_parenthesis_re; - - // Table of hash values for escaped characters: - public $escape_chars = '\`*_{}[]()>#+-.!'; - public $escape_chars_re; - - /** - * Markdown_Parser constructor. - */ - public function __construct() { - - // - // Constructor function. Initialize appropriate member variables. - // - $this->_initDetab(); - $this->prepareItalicsAndBold(); - - $this->nested_brackets_re = str_repeat( '(?>[^\[\]]+|\[', $this->nested_brackets_depth ) . str_repeat( '\])*', $this->nested_brackets_depth ); - - $this->nested_url_parenthesis_re = str_repeat( '(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth ) . str_repeat( '(?>\)))*', $this->nested_url_parenthesis_depth ); - - $this->escape_chars_re = '[' . preg_quote( $this->escape_chars ) . ']'; - - // Sort document, block, and span gamut in ascendent priority order. - asort( $this->document_gamut ); - asort( $this->block_gamut ); - asort( $this->span_gamut ); - } - - // Internal hashes used during transformation. - public $urls = array(); - public $titles = array(); - public $html_hashes = array(); - - // Status flag to avoid invalid nesting. - public $in_anchor = false; - - public function setup() { - - // - // Called before the transformation process starts to setup parser - // states. - // - // Clear global hashes. - $this->urls = $this->predef_urls; - $this->titles = $this->predef_titles; - $this->html_hashes = array(); - - $this->in_anchor = false; - } - - public function teardown() { - - // - // Called after the transformation process to clear any variable - // which may be taking up memory unnecessarly. - // - $this->urls = array(); - $this->titles = array(); - $this->html_hashes = array(); - } - - /** - * @param $text - * - * @return string - */ - public function transform( $text ) { - - // - // Main function. Performs some preprocessing on the input text - // and pass it through the document gamut. - // - $this->setup(); - - // Remove UTF-8 BOM and marker character in input, if present. - $text = preg_replace( '{^\xEF\xBB\xBF|\x1A}', '', $text ); - - // Standardize line endings: - // DOS to Unix and Mac to Unix - $text = preg_replace( '{\r\n?}', "\n", $text ); - - // Make sure $text ends with a couple of newlines: - $text .= "\n\n"; - - // Convert all tabs to spaces. - $text = $this->detab( $text ); - - // Turn block-level HTML blocks into hash entries - $text = $this->hashHTMLBlocks( $text ); - - // Strip any lines consisting only of spaces and tabs. - // This makes subsequent regexen easier to write, because we can - // match consecutive blank lines with /\n+/ instead of something - // contorted like /[ ]*\n+/ . - $text = preg_replace( '/^[ ]+$/m', '', $text ); - - // Run document gamut methods. - foreach ( $this->document_gamut as $method => $priority ) { - $text = $this->$method( $text ); - } - - $this->teardown(); - - return $text . "\n"; - } - - public $document_gamut = array( - // Strip link definitions, store in hashes. - 'stripLinkDefinitions' => 20, - - 'runBasicBlockGamut' => 30, - ); - - /** - * @param $text - * - * @return mixed - */ - public function stripLinkDefinitions( $text ) { - - // - // Strips link definitions from text, stores the URLs and titles in - // hash references. - // - $less_than_tab = $this->tab_width - 1; - - // Link defs are in the form: ^[id]: url "optional title" - $text = preg_replace_callback( '{ - ^[ ]{0,' . $less_than_tab . '}\[(.+)\][ ]?: # id = $1 - [ ]* - \n? # maybe *one* newline - [ ]* - (?: - <(.+?)> # url = $2 - | - (\S+?) # url = $3 - ) - [ ]* - \n? # maybe one newline - [ ]* - (?: - (?<=\s) # lookbehind for whitespace - ["(] - (.*?) # title = $4 - [")] - [ ]* - )? # title is optional - (?:\n+|\Z) - }xm', array( &$this, '_stripLinkDefinitions_callback' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _stripLinkDefinitions_callback( $matches ) { - - $link_id = strtolower( $matches[1] ); - $url = $matches[2] == '' ? $matches[3] : $matches[2]; - $this->urls[ $link_id ] = $url; - $this->titles[ $link_id ] =& $matches[4]; - - return ''; - // String that will replace the block - } - - /** - * @param $text - * - * @return mixed - */ - public function hashHTMLBlocks( $text ) { - - if ( $this->no_markup ) { - return $text; - } - - $less_than_tab = $this->tab_width - 1; - - // Hashify HTML blocks: - // We only want to do this for block-level HTML tags, such as headers, - // lists, and tables. That's because we still want to wrap

    s around - // "paragraphs" that are wrapped in non-block-level tags, such as anchors, - // phrase emphasis, and spans. The list of tags we're looking for is - // hard-coded: - // - // * List "a" is made of tags which can be both inline or block-level. - // These will be treated block-level when the start tag is alone on - // its line, otherwise they're not matched here and will be taken as - // inline later. - // * List "b" is made of tags which are always block-level; - // - $block_tags_a_re = 'ins|del'; - $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|' . 'script|noscript|form|fieldset|iframe|math|svg|' . 'article|section|nav|aside|hgroup|header|footer|' . 'figure'; - - // Regular expression for the content of a block tag. - $nested_tags_level = 4; - $attr = ' - (?> # optional tag attributes - \s # starts with whitespace - (?> - [^>"/]+ # text outside quotes - | - /+(?!>) # slash not followed by ">" - | - "[^"]*" # text inside double quotes (tolerate ">") - | - \'[^\']*\' # text inside single quotes (tolerate ">") - )* - )? - '; - $content = str_repeat( ' - (?> - [^<]+ # content without tag - | - <\2 # nested opening tag - ' . $attr . ' # attributes - (?> - /> - | - >', $nested_tags_level ) . // end of opening tag - '.*?' . // last level nested tag content - str_repeat( ' - # closing nested tag - ) - | - <(?!/\2\s*> # other tags with a different name - ) - )*', $nested_tags_level ); - $content2 = str_replace( '\2', '\3', $content ); - - // First, look for nested blocks, e.g.: - //

    - //
    - // tags for inner block must be indented. - //
    - //
    - // - // The outermost tags must start at the left margin for this to match, and - // the inner nested divs must be indented. - // We need to do this before the next, more liberal match, because the next - // match will start at the first `
    ` and stop at the first `
    `. - $text = preg_replace_callback( '{(?> - (?> - (?<=\n\n) # Starting after a blank line - | # or - \A\n? # the beginning of the doc - ) - ( # save in $1 - - # Match from `\n` to `\n`, handling nested tags - # in between. - - [ ]{0,' . $less_than_tab . '} - <(' . $block_tags_b_re . ')# start tag = $2 - ' . $attr . '> # attributes followed by > and \n - ' . $content . ' # content, support nesting - # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special version for tags of group a. - - [ ]{0,' . $less_than_tab . '} - <(' . $block_tags_a_re . ')# start tag = $3 - ' . $attr . '>[ ]*\n # attributes followed by > - ' . $content2 . ' # content, support nesting - # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special case just for
    . It was easier to make a special - # case than to make the other regex more complicated. - - [ ]{0,' . $less_than_tab . '} - <(hr) # start tag = $2 - ' . $attr . ' # attributes - /?> # the matching end tag - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # Special case for standalone HTML comments: - - [ ]{0,' . $less_than_tab . '} - (?s: - - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # PHP and ASP-style processor instructions ( - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - ) - )}Sxmi', array( &$this, '_hashHTMLBlocks_callback' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _hashHTMLBlocks_callback( $matches ) { - - $text = $matches[1]; - $key = $this->hashBlock( $text ); - - return "\n\n$key\n\n"; - } - - /** - * @param $text - * @param string $boundary - * - * @return string - */ - public function hashPart( $text, $boundary = 'X' ) { - - // - // Called whenever a tag must be hashed when a function insert an atomic - // element in the text stream. Passing $text to through this function gives - // a unique text-token which will be reverted back when calling unhash. - // - // The $boundary argument specify what character should be used to surround - // the token. By convension, "B" is used for block elements that needs not - // to be wrapped into paragraph tags at the end, ":" is used for elements - // that are word separators and "X" is used in the general case. - // - // Swap back any tag hash found in $text so we do not have to `unhash` - // multiple times at the end. - $text = $this->unhash( $text ); - - // Then hash the block. - static $i = 0; - $key = "$boundary\x1A" . ++ $i . $boundary; - $this->html_hashes[ $key ] = $text; - - return $key; - // String that will replace the tag. - } - - /** - * @param $text - * - * @return string - */ - public function hashBlock( $text ) { - - // - // Shortcut function for hashPart with block-level boundaries. - // - return $this->hashPart( $text, 'B' ); - } - - public $block_gamut = array( - // - // These are all the transformations that form block-level - // tags like paragraphs, headers, and list items. - // - 'doHeaders' => 10, - 'doHorizontalRules' => 20, - - 'doLists' => 40, - 'doCodeBlocks' => 50, - 'doBlockQuotes' => 60, - ); - - /** - * @param $text - * - * @return string - */ - public function runBlockGamut( $text ) { - - // - // Run block gamut tranformations. - // - // We need to escape raw HTML in Markdown source before doing anything - // else. This need to be done for each block, and not only at the - // begining in the Markdown function since hashed blocks can be part of - // list items and could have been indented. Indented blocks would have - // been seen as a code block in a previous pass of hashHTMLBlocks. - $text = $this->hashHTMLBlocks( $text ); - - return $this->runBasicBlockGamut( $text ); - } - - /** - * @param $text - * - * @return string - */ - public function runBasicBlockGamut( $text ) { - - // - // Run block gamut tranformations, without hashing HTML blocks. This is - // useful when HTML blocks are known to be already hashed, like in the first - // whole-document pass. - // - foreach ( $this->block_gamut as $method => $priority ) { - $text = $this->$method( $text ); - } - - // Finally form paragraph and restore hashed blocks. - $text = $this->formParagraphs( $text ); - - return $text; - } - - /** - * @param $text - * - * @return mixed - */ - public function doHorizontalRules( $text ) { - - // Do Horizontal Rules: - return preg_replace( '{ - ^[ ]{0,3} # Leading space - ([-*_]) # $1: First marker - (?> # Repeated marker group - [ ]{0,2} # Zero, one, or two spaces. - \1 # Marker character - ){2,} # Group repeated at least twice - [ ]* # Tailing spaces - $ # End of line. - }mx', "\n" . $this->hashBlock( "empty_element_suffix" ) . "\n", $text ); - } - - public $span_gamut = array( - // - // These are all the transformations that occur *within* block-level - // tags like paragraphs, headers, and list items. - // - // Process character escapes, code spans, and inline HTML - // in one shot. - 'parseSpan' => - 30, - - // Process anchor and image tags. Images must come first, - // because ![foo][f] looks like an anchor. - 'doImages' => 10, - 'doAnchors' => 20, - - // Make links out of things like `` - // Must come after doAnchors, because you can use < and > - // delimiters in inline links like [this](). - 'doAutoLinks' => 30, - 'encodeAmpsAndAngles' => 40, - - 'doItalicsAndBold' => 50, - 'doHardBreaks' => 60, - ); - - /** - * @param $text - * - * @return mixed - */ - public function runSpanGamut( $text ) { - - // - // Run span gamut tranformations. - // - foreach ( $this->span_gamut as $method => $priority ) { - $text = $this->$method( $text ); - } - - return $text; - } - - /** - * @param $text - * - * @return mixed - */ - public function doHardBreaks( $text ) { - - // Do hard breaks: - return preg_replace_callback( '/ {2,}\n/', array( &$this, '_doHardBreaks_callback' ), $text ); - } - - /** - * @param $matches - * - * @return string - */ - public function _doHardBreaks_callback( $matches ) { - - return $this->hashPart( "empty_element_suffix\n" ); - } - - /** - * @param $text - * - * @return mixed - */ - public function doAnchors( $text ) { - - // - // Turn Markdown link shortcuts into XHTML tags. - // - if ( $this->in_anchor ) { - return $text; - } - $this->in_anchor = true; - - // - // First, handle reference-style links: [link text] [id] - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - \[ - (' . $this->nested_brackets_re . ') # link text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - ) - }xs', array( &$this, '_doAnchors_reference_callback' ), $text ); - - // - // Next, inline-style links: [link text](url "optional title") - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - \[ - (' . $this->nested_brackets_re . ') # link text = $2 - \] - \( # literal paren - [ \n]* - (?: - <(.+?)> # href = $3 - | - (' . $this->nested_url_parenthesis_re . ') # href = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # Title = $7 - \6 # matching quote - [ \n]* # ignore any spaces/tabs between closing quote and ) - )? # title is optional - \) - ) - }xs', array( &$this, '_doAnchors_inline_callback' ), $text ); - - // - // Last, handle reference-style shortcuts: [link text] - // These must come last in case you've also got [link text][1] - // or [link text](/foo) - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - \[ - ([^\[\]]+) # link text = $2; can\'t contain [ or ] - \] - ) - }xs', array( &$this, '_doAnchors_reference_callback' ), $text ); - - $this->in_anchor = false; - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doAnchors_reference_callback( $matches ) { - - $whole_match = $matches[1]; - $link_text = $matches[2]; - $link_id =& $matches[3]; - - if ( $link_id == '' ) { - // for shortcut links like [this][] or [this]. - $link_id = $link_text; - } - - // lower-case and turn embedded newlines into spaces - $link_id = strtolower( $link_id ); - $link_id = preg_replace( '{[ ]?\n}', ' ', $link_id ); - - if ( isset( $this->urls[ $link_id ] ) ) { - $url = $this->urls[ $link_id ]; - $url = $this->encodeAttribute( $url ); - - $result = "titles[ $link_id ] ) ) { - $title = $this->titles[ $link_id ]; - $title = $this->encodeAttribute( $title ); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut( $link_text ); - $result .= ">$link_text"; - $result = $this->hashPart( $result ); - } else { - $result = $whole_match; - } - - return $result; - } - - /** - * @param $matches - * - * @return string - */ - public function _doAnchors_inline_callback( $matches ) { - - $whole_match = $matches[1]; - $link_text = $this->runSpanGamut( $matches[2] ); - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $url = $this->encodeAttribute( $url ); - - $result = "encodeAttribute( $title ); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut( $link_text ); - $result .= ">$link_text"; - - return $this->hashPart( $result ); - } - - /** - * @param $text - * - * @return mixed - */ - public function doImages( $text ) { - - // - // Turn Markdown image shortcuts into tags. - // - // - // First, handle reference-style labeled images: ![alt text][id] - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - !\[ - (' . $this->nested_brackets_re . ') # alt text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - - ) - }xs', array( &$this, '_doImages_reference_callback' ), $text ); - - // - // Next, handle inline images: ![alt text](url "optional title") - // Don't forget: encode * and _ - // - $text = preg_replace_callback( '{ - ( # wrap whole match in $1 - !\[ - (' . $this->nested_brackets_re . ') # alt text = $2 - \] - \s? # One optional whitespace character - \( # literal paren - [ \n]* - (?: - <(\S*)> # src url = $3 - | - (' . $this->nested_url_parenthesis_re . ') # src url = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # title = $7 - \6 # matching quote - [ \n]* - )? # title is optional - \) - ) - }xs', array( &$this, '_doImages_inline_callback' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doImages_reference_callback( $matches ) { - - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower( $matches[3] ); - - if ( $link_id == '' ) { - $link_id = strtolower( $alt_text ); - // for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute( $alt_text ); - if ( isset( $this->urls[ $link_id ] ) ) { - $url = $this->encodeAttribute( $this->urls[ $link_id ] ); - $result = "\"$alt_text\"";titles[ $link_id ] ) ) { - $title = $this->titles[ $link_id ]; - $title = $this->encodeAttribute( $title ); - $result .= " title=\"$title\""; - } - $result .= $this->empty_element_suffix; - $result = $this->hashPart( $result ); - } else { - // If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - - /** - * @param $matches - * - * @return string - */ - public function _doImages_inline_callback( $matches ) { - - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $alt_text = $this->encodeAttribute( $alt_text ); - $url = $this->encodeAttribute( $url ); - $result = "\"$alt_text\"";encodeAttribute( $title ); - $result .= " title=\"$title\""; - // $title already quoted - } - $result .= $this->empty_element_suffix; - - return $this->hashPart( $result ); - } - - /** - * @param $text - * - * @return mixed - */ - public function doHeaders( $text ) { - - // Setext-style headers: - // Header 1 - // ======== - // - // Header 2 - // -------- - // - $text = preg_replace_callback( '{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', array( - &$this, - '_doHeaders_callback_setext', - ), $text ); - - // atx-style headers: - // Header 1 - // Header 2 - // Header 2 with closing hashes ## - // ... - // Header 6 - // - $text = preg_replace_callback( '{ - ^(\#{1,6}) # $1 = string of #\'s - [ ]* - (.+?) # $2 = Header text - [ ]* - \#* # optional closing #\'s (not counted) - \n+ - }xm', array( &$this, '_doHeaders_callback_atx' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doHeaders_callback_setext( $matches ) { - - // Terrible hack to check we haven't found an empty list item. - if ( $matches[2] == '-' && preg_match( '{^-(?: |$)}', $matches[1] ) ) { - return $matches[0]; - } - - $level = $matches[2]{0} == '=' ? 1 : 2; - $block = "" . $this->runSpanGamut( $matches[1] ) . ""; - - return "\n" . $this->hashBlock( $block ) . "\n\n"; - } - - /** - * @param $matches - * - * @return string - */ - public function _doHeaders_callback_atx( $matches ) { - - $level = strlen( $matches[1] ); - $block = "" . $this->runSpanGamut( $matches[2] ) . ""; - - return "\n" . $this->hashBlock( $block ) . "\n\n"; - } - - /** - * @param $text - * - * @return mixed - */ - public function doLists( $text ) { - - // - // Form HTML ordered (numbered) and unordered (bulleted) lists. - // - $less_than_tab = $this->tab_width - 1; - - // Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; - - $markers_relist = array( - $marker_ul_re => $marker_ol_re, - $marker_ol_re => $marker_ul_re, - ); - - foreach ( $markers_relist as $marker_re => $other_marker_re ) { - // Re-usable pattern to match any entirel ul or ol list: - $whole_list_re = ' - ( # $1 = whole list - ( # $2 - ([ ]{0,' . $less_than_tab . '}) # $3 = number of spaces - (' . $marker_re . ') # $4 = first list item marker - [ ]+ - ) - (?s:.+?) - ( # $5 - \z - | - \n{2,} - (?=\S) - (?! # Negative lookahead for another list item marker - [ ]* - ' . $marker_re . '[ ]+ - ) - | - (?= # Lookahead for another kind of list - \n - \3 # Must have the same indentation - ' . $other_marker_re . '[ ]+ - ) - ) - ) - '; - // mx - // We use a different prefix before nested lists than top-level lists. - // See extended comment in _ProcessListItems(). - if ( $this->list_level ) { - $text = preg_replace_callback( '{ - ^ - ' . $whole_list_re . ' - }mx', array( &$this, '_doLists_callback' ), $text ); - } else { - $text = preg_replace_callback( '{ - (?:(?<=\n)\n|\A\n?) # Must eat the newline - ' . $whole_list_re . ' - }mx', array( &$this, '_doLists_callback' ), $text ); - } - }//end foreach - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doLists_callback( $matches ) { - - // Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; - - $list = $matches[1]; - $list_type = preg_match( "/$marker_ul_re/", $matches[4] ) ? 'ul' : 'ol'; - - $marker_any_re = ( $list_type == 'ul' ? $marker_ul_re : $marker_ol_re ); - - $list .= "\n"; - $result = $this->processListItems( $list, $marker_any_re ); - - $result = $this->hashBlock( "<$list_type>\n" . $result . "" ); - - return "\n" . $result . "\n\n"; - } - - public $list_level = 0; - - /** - * @param $list_str - * @param $marker_any_re - * - * @return mixed - */ - public function processListItems( $list_str, $marker_any_re ) { - - // - // Process the contents of a single ordered or unordered list, splitting it - // into individual list items. - // - // The $this->list_level global keeps track of when we're inside a list. - // Each time we enter a list, we increment it; when we leave a list, - // we decrement. If it's zero, we're not in a list anymore. - // - // We do this because when we're not inside a list, we want to treat - // something like this: - // - // I recommend upgrading to version - // 8. Oops, now this line is treated - // as a sub-list. - // - // As a single paragraph, despite the fact that the second line starts - // with a digit-period-space sequence. - // - // Whereas when we're inside a list (or sub-list), that line will be - // treated as the start of a sub-list. What a kludge, huh? This is - // an aspect of Markdown's syntax that's hard to parse perfectly - // without resorting to mind-reading. Perhaps the solution is to - // change the syntax rules such that sub-lists must start with a - // starting cardinal number; e.g. "1." or "a.". - $this->list_level ++; - - // trim trailing blank lines: - $list_str = preg_replace( "/\n{2,}\\z/", "\n", $list_str ); - - $list_str = preg_replace_callback( '{ - (\n)? # leading line = $1 - (^[ ]*) # leading whitespace = $2 - (' . $marker_any_re . ' # list marker and space = $3 - (?:[ ]+|(?=\n)) # space only required if item is not empty - ) - ((?s:.*?)) # list item text = $4 - (?:(\n+(?=\n))|\n) # tailing blank line = $5 - (?= \n* (\z | \2 (' . $marker_any_re . ') (?:[ ]+|(?=\n)))) - }xm', array( &$this, '_processListItems_callback' ), $list_str ); - - $this->list_level --; - - return $list_str; - } - - /** - * @param $matches - * - * @return string - */ - public function _processListItems_callback( $matches ) { - - $item = $matches[4]; - $leading_line =& $matches[1]; - $leading_space =& $matches[2]; - $marker_space = $matches[3]; - $tailing_blank_line =& $matches[5]; - - if ( $leading_line || $tailing_blank_line || preg_match( '/\n{2,}/', $item ) ) { - // Replace marker with the appropriate whitespace indentation - $item = $leading_space . str_repeat( ' ', strlen( $marker_space ) ) . $item; - $item = $this->runBlockGamut( $this->outdent( $item ) . "\n" ); - } else { - // Recursion for sub-lists: - $item = $this->doLists( $this->outdent( $item ) ); - $item = preg_replace( '/\n+$/', '', $item ); - $item = $this->runSpanGamut( $item ); - } - - return '
  • ' . $item . "
  • \n"; - } - - /** - * @param $text - * - * @return mixed - */ - public function doCodeBlocks( $text ) { - - // - // Process Markdown `
    ` blocks.
    -			//
    -			$text = preg_replace_callback( '{
    -				(?:\n\n|\A\n?)
    -				(	            # $1 = the code block -- one or more lines, starting with a space/tab
    -				  (?>
    -					[ ]{' . $this->tab_width . '}  # Lines must start with a tab or a tab-width of spaces
    -					.*\n+
    -				  )+
    -				)
    -				((?=^[ ]{0,' . $this->tab_width . '}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
    -			}xm', array( &$this, '_doCodeBlocks_callback' ), $text );
    -
    -			return $text;
    -		}
    -
    -		/**
    -		 * @param $matches
    -		 *
    -		 * @return string
    -		 */
    -		public function _doCodeBlocks_callback( $matches ) {
    -
    -			$codeblock = $matches[1];
    -
    -			$codeblock = $this->outdent( $codeblock );
    -			$codeblock = htmlspecialchars( $codeblock, ENT_NOQUOTES );
    -
    -			// trim leading newlines and trailing newlines
    -			$codeblock = preg_replace( '/\A\n+|\n+\z/', '', $codeblock );
    -
    -			$codeblock = "
    $codeblock\n
    "; - - return "\n\n" . $this->hashBlock( $codeblock ) . "\n\n"; - } - - /** - * @param $code - * - * @return string - */ - public function makeCodeSpan( $code ) { - - // - // Create a code span markup for $code. Called from handleSpanToken. - // - $code = htmlspecialchars( trim( $code ), ENT_NOQUOTES ); - - return $this->hashPart( "$code" ); - } - - public $em_relist = array( - '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?em_relist as $em => $em_re ) { - foreach ( $this->strong_relist as $strong => $strong_re ) { - // Construct list of allowed token expressions. - $token_relist = array(); - if ( isset( $this->em_strong_relist["$em$strong"] ) ) { - $token_relist[] = $this->em_strong_relist["$em$strong"]; - } - $token_relist[] = $em_re; - $token_relist[] = $strong_re; - - // Construct master expression from list. - $token_re = '{(' . implode( '|', $token_relist ) . ')}'; - $this->em_strong_prepared_relist["$em$strong"] = $token_re; - } - } - } - - /** - * @param $text - * - * @return string - */ - public function doItalicsAndBold( $text ) { - - $token_stack = array( '' ); - $text_stack = array( '' ); - $em = ''; - $strong = ''; - $tree_char_em = false; - - while ( 1 ) { - // - // Get prepared regular expression for seraching emphasis tokens - // in current context. - // - $token_re = $this->em_strong_prepared_relist["$em$strong"]; - - // - // Each loop iteration search for the next emphasis token. - // Each token is then passed to handleSpanToken. - // - $parts = preg_split( $token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE ); - $text_stack[0] .= $parts[0]; - $token =& $parts[1]; - $text =& $parts[2]; - - if ( empty( $token ) ) { - // Reached end of text span: empty stack without emitting. - // any more emphasis. - while ( $token_stack[0] ) { - $text_stack[1] .= array_shift( $token_stack ); - $text_stack[0] .= array_shift( $text_stack ); - } - break; - } - - $token_len = strlen( $token ); - if ( $tree_char_em ) { - // Reached closing marker while inside a three-char emphasis. - if ( $token_len == 3 ) { - // Three-char closing marker, close em and strong. - array_shift( $token_stack ); - $span = array_shift( $text_stack ); - $span = $this->runSpanGamut( $span ); - $span = "$span"; - $text_stack[0] .= $this->hashPart( $span ); - $em = ''; - $strong = ''; - } else { - // Other closing marker: close one em or strong and - // change current token state to match the other - $token_stack[0] = str_repeat( $token{0}, 3 - $token_len ); - $tag = $token_len == 2 ? 'strong' : 'em'; - $span = $text_stack[0]; - $span = $this->runSpanGamut( $span ); - $span = "<$tag>$span"; - $text_stack[0] = $this->hashPart( $span ); - $$tag = ''; - // $$tag stands for $em or $strong - }//end if - $tree_char_em = false; - } elseif ( $token_len == 3 ) { - if ( $em ) { - // Reached closing marker for both em and strong. - // Closing strong marker: - for ( $i = 0; $i < 2; ++ $i ) { - $shifted_token = array_shift( $token_stack ); - $tag = strlen( $shifted_token ) == 2 ? 'strong' : 'em'; - $span = array_shift( $text_stack ); - $span = $this->runSpanGamut( $span ); - $span = "<$tag>$span"; - $text_stack[0] .= $this->hashPart( $span ); - $$tag = ''; - // $$tag stands for $em or $strong - } - } else { - // Reached opening three-char emphasis marker. Push on token - // stack; will be handled by the special condition above. - $em = $token{0}; - $strong = "$em$em"; - array_unshift( $token_stack, $token ); - array_unshift( $text_stack, '' ); - $tree_char_em = true; - }//end if - } elseif ( $token_len == 2 ) { - if ( $strong ) { - // Unwind any dangling emphasis marker: - if ( strlen( $token_stack[0] ) == 1 ) { - $text_stack[1] .= array_shift( $token_stack ); - $text_stack[0] .= array_shift( $text_stack ); - } - // Closing strong marker: - array_shift( $token_stack ); - $span = array_shift( $text_stack ); - $span = $this->runSpanGamut( $span ); - $span = "$span"; - $text_stack[0] .= $this->hashPart( $span ); - $strong = ''; - } else { - array_unshift( $token_stack, $token ); - array_unshift( $text_stack, '' ); - $strong = $token; - } - } else { - // Here $token_len == 1 - if ( $em ) { - if ( strlen( $token_stack[0] ) == 1 ) { - // Closing emphasis marker: - array_shift( $token_stack ); - $span = array_shift( $text_stack ); - $span = $this->runSpanGamut( $span ); - $span = "$span"; - $text_stack[0] .= $this->hashPart( $span ); - $em = ''; - } else { - $text_stack[0] .= $token; - } - } else { - array_unshift( $token_stack, $token ); - array_unshift( $text_stack, '' ); - $em = $token; - } - }//end if - }//end while - - return $text_stack[0]; - } - - /** - * @param $text - * - * @return mixed - */ - public function doBlockQuotes( $text ) { - - $text = preg_replace_callback( '/ - ( # Wrap whole match in $1 - (?> - ^[ ]*>[ ]? # ">" at the start of a line - .+\n # rest of the first line - (.+\n)* # subsequent consecutive lines - \n* # blanks - )+ - ) - /xm', array( &$this, '_doBlockQuotes_callback' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doBlockQuotes_callback( $matches ) { - - $bq = $matches[1]; - // trim one level of quoting - trim whitespace-only lines - $bq = preg_replace( '/^[ ]*>[ ]?|^[ ]+$/m', '', $bq ); - $bq = $this->runBlockGamut( $bq ); - // recurse - $bq = preg_replace( '/^/m', ' ', $bq ); - // These leading spaces cause problem with
     content,
    -			// so we need to fix that:
    -			$bq = preg_replace_callback( '{(\s*
    .+?
    )}sx', array( &$this, '_doBlockQuotes_callback2' ), $bq ); - - return "\n" . $this->hashBlock( "
    \n$bq\n
    " ) . "\n\n"; - } - - /** - * @param $matches - * - * @return mixed - */ - public function _doBlockQuotes_callback2( $matches ) { - - $pre = $matches[1]; - $pre = preg_replace( '/^ /m', '', $pre ); - - return $pre; - } - - /** - * @param $text - * - * @return string - */ - public function formParagraphs( $text ) { - - // - // Params: - // $text - string to process with html

    tags - // - // Strip leading and trailing lines: - $text = preg_replace( '/\A\n+|\n+\z/', '', $text ); - - $grafs = preg_split( '/\n{2,}/', $text, - 1, PREG_SPLIT_NO_EMPTY ); - - // - // Wrap

    tags and unhashify HTML blocks - // - foreach ( $grafs as $key => $value ) { - if ( ! preg_match( '/^B\x1A[0-9]+B$/', $value ) ) { - // Is a paragraph. - $value = $this->runSpanGamut( $value ); - $value = preg_replace( '/^([ ]*)/', '

    ', $value ); - $value .= '

    '; - $grafs[ $key ] = $this->unhash( $value ); - } else { - // Is a block. - // Modify elements of @grafs in-place... - $graf = $value; - $block = $this->html_hashes[ $graf ]; - $graf = $block; - // if (preg_match('{ - // \A - // ( # $1 =
    tag - //
    ]* - // \b - // markdown\s*=\s* ([\'"]) # $2 = attr quote char - // 1 - // \2 - // [^>]* - // > - // ) - // ( # $3 = contents - // .* - // ) - // (
    ) # $4 = closing tag - // \z - // }xs', $block, $matches)) - // { - // list(, $div_open, , $div_content, $div_close) = $matches; - // - // # We can't call Markdown(), because that resets the hash; - // # that initialization code should be pulled into its own sub, though. - // $div_content = $this->hashHTMLBlocks($div_content); - // - // # Run document gamut methods on the content. - // foreach ($this->document_gamut as $method => $priority) { - // $div_content = $this->$method($div_content); - // } - // - // $div_open = preg_replace( - // '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); - // - // $graf = $div_open . "\n" . $div_content . "\n" . $div_close; - // } - $grafs[ $key ] = $graf; - }//end if - }//end foreach - - return implode( "\n\n", $grafs ); - } - - /** - * @param $text - * - * @return mixed - */ - public function encodeAttribute( $text ) { - - // - // Encode text for a double-quoted HTML attribute. This function - // is *not* suitable for attributes enclosed in single quotes. - // - $text = $this->encodeAmpsAndAngles( $text ); - $text = str_replace( '"', '"', $text ); - - return $text; - } - - /** - * @param $text - * - * @return mixed - */ - public function encodeAmpsAndAngles( $text ) { - - // - // Smart processing for ampersands and angle brackets that need to - // be encoded. Valid character entities are left alone unless the - // no-entities mode is set. - // - if ( $this->no_entities ) { - $text = str_replace( '&', '&', $text ); - } else { - // Ampersand-encoding based entirely on Nat Irons's Amputator - // MT plugin: - $text = preg_replace( '/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', '&', $text ); - } - // Encode remaining <'s - $text = str_replace( '<', '<', $text ); - - return $text; - } - - /** - * @param $text - * - * @return mixed - */ - public function doAutoLinks( $text ) { - - $text = preg_replace_callback( '{<((https?|ftp|dict):[^\'">\s]+)>}i', array( - &$this, - '_doAutoLinks_url_callback', - ), $text ); - - // Email addresses: - $text = preg_replace_callback( '{ - < - (?:mailto:)? - ( - (?: - [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ - | - ".*?" - ) - \@ - (?: - [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ - | - \[[\d.a-fA-F:]+\] # IPv4 & IPv6 - ) - ) - > - }xi', array( &$this, '_doAutoLinks_email_callback' ), $text ); - $text = preg_replace_callback( '{<(tel:([^\'">\s]+))>}i', array( - &$this, - '_doAutoLinks_tel_callback', - ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _doAutoLinks_tel_callback( $matches ) { - - $url = $this->encodeAttribute( $matches[1] ); - $tel = $this->encodeAttribute( $matches[2] ); - $link = "$tel"; - - return $this->hashPart( $link ); - } - - /** - * @param $matches - * - * @return string - */ - public function _doAutoLinks_url_callback( $matches ) { - - $url = $this->encodeAttribute( $matches[1] ); - $link = "$url"; - - return $this->hashPart( $link ); - } - - /** - * @param $matches - * - * @return string - */ - public function _doAutoLinks_email_callback( $matches ) { - - $address = $matches[1]; - $link = $this->encodeEmailAddress( $address ); - - return $this->hashPart( $link ); - } - - /** - * @param $addr - * - * @return string - */ - public function encodeEmailAddress( $addr ) { - - // - // Input: an email address, e.g. "foo@example.com" - // - // Output: the email address as a mailto link, with each character - // of the address encoded as either a decimal or hex entity, in - // the hopes of foiling most address harvesting spam bots. E.g.: - // - //

    foo@exampl - // e.com

    - // - // Based by a filter by Matthew Wickline, posted to BBEdit-Talk. - // With some optimizations by Milian Wolff. - // - $addr = 'mailto:' . $addr; - $chars = preg_split( '/(? $char ) { - $ord = ord( $char ); - // Ignore non-ascii chars. - if ( $ord < 128 ) { - $r = ( $seed * ( 1 + $key ) ) % 100; - // Pseudo-random function. - // roughly 10% raw, 45% hex, 45% dec - // '@' *must* be encoded. I insist. - if ( $r > 90 && $char != '@' ) { /* do nothing */ - } elseif ( $r < 45 ) { - $chars[ $key ] = '&#x' . dechex( $ord ) . ';'; - } else { - $chars[ $key ] = '&#' . $ord . ';'; - } - } - } - - $addr = implode( '', $chars ); - $text = implode( '', array_slice( $chars, 7 ) ); - // text without `mailto:` - $addr = "$text"; - - return $addr; - } - - /** - * @param $str - * - * @return string - */ - public function parseSpan( $str ) { - - // - // Take the string $str and parse it into tokens, hashing embeded HTML, - // escaped characters and handling code spans. - // - $output = ''; - - $span_re = '{ - ( - \\\\' . $this->escape_chars_re . ' - | - (?no_markup ? '' : ' - | - # comment - | - <\?.*?\?> | <%.*?%> # processing instruction - | - <[!$]?[-a-zA-Z0-9:_]+ # regular tags - (?> - \s - (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* - )? - > - | - <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag - | - # closing tag - ' ) . ' - ) - }xs'; - - while ( 1 ) { - // - // Each loop iteration seach for either the next tag, the next - // openning code span marker, or the next escaped character. - // Each token is then passed to handleSpanToken. - // - $parts = preg_split( $span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE ); - - // Create token from text preceding tag. - if ( $parts[0] != '' ) { - $output .= $parts[0]; - } - - // Check if we reach the end. - if ( isset( $parts[1] ) ) { - $output .= $this->handleSpanToken( $parts[1], $parts[2] ); - $str = $parts[2]; - } else { - break; - } - }//end while - - return $output; - } - - /** - * @param $token - * @param $str - * - * @return string - */ - public function handleSpanToken( $token, &$str ) { - - // - // Handle $token provided by parseSpan by determining its nature and - // returning the corresponding value that should replace it. - // - switch ( $token{0} ) { - case '\\': - return $this->hashPart( '&#' . ord( $token{1} ) . ';' ); - case '`': - // Search for end marker in remaining text. - if ( preg_match( '/^(.*?[^`])' . preg_quote( $token ) . '(?!`)(.*)$/sm', $str, $matches ) ) { - $str = $matches[2]; - $codespan = $this->makeCodeSpan( $matches[1] ); - - return $this->hashPart( $codespan ); - } - - return $token; - // return as text since no ending marker found. - default: - return $this->hashPart( $token ); - } - } - - /** - * @param $text - * - * @return mixed - */ - public function outdent( $text ) { - - // - // Remove one level of line-leading tabs or spaces - // - return preg_replace( '/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text ); - } - - - // String length function for detab. `_initDetab` will create a function to - // hanlde UTF-8 if the default function does not exist. - public $utf8_strlen = 'mb_strlen'; - - /** - * @param $text - * - * @return mixed - */ - public function detab( $text ) { - - // - // Replace tabs with the appropriate amount of space. - // - // For each line we separate the line in blocks delemited by - // tab characters. Then we reconstruct every line by adding the - // appropriate number of space between each blocks. - $text = preg_replace_callback( '/^.*\t.*$/m', array( &$this, '_detab_callback' ), $text ); - - return $text; - } - - /** - * @param $matches - * - * @return string - */ - public function _detab_callback( $matches ) { - - $line = $matches[0]; - $strlen = $this->utf8_strlen; - // strlen function for UTF-8. - // Split in blocks. - $blocks = explode( "\t", $line ); - // Add each blocks to the line. - $line = $blocks[0]; - unset( $blocks[0] ); - // Do not add first block twice. - foreach ( $blocks as $block ) { - // Calculate amount of space, insert spaces, insert block. - $amount = $this->tab_width - $strlen( $line, 'UTF-8' ) % $this->tab_width; - $line .= str_repeat( ' ', $amount ) . $block; - } - - return $line; - } - - public function _initDetab() { - - // - // Check for the availability of the function in the `utf8_strlen` property - // (initially `mb_strlen`). If the function is not available, create a - // function that will loosely count the number of UTF-8 characters with a - // regular expression. - // - if ( function_exists( $this->utf8_strlen ) ) { - return; - } - $this->utf8_strlen = create_function( '$text', 'return preg_match_all( - "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", - $text, $m);' ); - } - - /** - * @param $text - * - * @return mixed - */ - public function unhash( $text ) { - - // - // Swap back in all the tags hashed by _HashHTMLBlocks. - // - return preg_replace_callback( '/(.)\x1A[0-9]+\1/', array( &$this, '_unhash_callback' ), $text ); - } - - /** - * @param $matches - * - * @return mixed - */ - public function _unhash_callback( $matches ) { - - return $this->html_hashes[ $matches[0] ]; - } - - } - - /* - PHP Markdown - ============ - - Description - ----------- - - This is a PHP translation of the original Markdown formatter written in - Perl by John Gruber. - - Markdown is a text-to-HTML filter; it translates an easy-to-read / - easy-to-write structured text format into HTML. Markdown's text format - is mostly similar to that of plain text email, and supports features such - as headers, *emphasis*, code blocks, blockquotes, and links. - - Markdown's syntax is designed not as a generic markup language, but - specifically to serve as a front-end to (X)HTML. You can use span-level - HTML tags anywhere in a Markdown document, and you can use block level - HTML tags (like
    and as well). - - For more information about Markdown's syntax, see: - - - - - Bugs - ---- - - To file bug reports please send email to: - - - - Please include with your report: (1) the example input; (2) the output you - expected; (3) the output Markdown actually produced. - - - Version History - --------------- - - See the readme file for detailed release notes for this version. - - - Copyright and License - --------------------- - - PHP Markdown - Copyright (c) 2004-2013 Michel Fortin - - All rights reserved. - - Based on Markdown - Copyright (c) 2003-2006 John Gruber - - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - * Neither the name "Markdown" nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. - - This software is provided by the copyright holders and contributors "as - is" and any express or implied warranties, including, but not limited - to, the implied warranties of merchantability and fitness for a - particular purpose are disclaimed. In no event shall the copyright owner - or contributors be liable for any direct, indirect, incidental, special, - exemplary, or consequential damages (including, but not limited to, - procurement of substitute goods or services; loss of use, data, or - profits; or business interruption) however caused and on any theory of - liability, whether in contract, strict liability, or tort (including - negligence or otherwise) arising in any way out of the use of this - software, even if advised of the possibility of such damage. - - */ -endif; diff --git a/includes/Pods_GF.php b/includes/Pods_GF.php index 2a59e09..f4e66a8 100644 --- a/includes/Pods_GF.php +++ b/includes/Pods_GF.php @@ -905,6 +905,7 @@ public static function secondary_submits( $form_id, $options = array() ) { if ( ! has_filter( 'gform_submit_button_' . $form_id, array( 'Pods_GF', 'gf_secondary_submit_button' ) ) ) { add_filter( 'gform_submit_button_' . $form_id, array( 'Pods_GF', 'gf_secondary_submit_button' ), 10, 2 ); + add_filter( 'gform_get_form_filter_' . $form_id, array( 'Pods_GF', 'gf_secondary_submit_form' ), 10, 2 ); } if ( ! wp_script_is( 'pods-gf', 'registered' ) ) { @@ -929,7 +930,139 @@ public static function secondary_submits( $form_id, $options = array() ) { */ public static function gf_secondary_submit_button( $button_input, $form ) { - $secondary_submits = pods_v( $form['id'], self::$secondary_submits, array(), true ); + $form_id = $form['id']; + + $secondary_submits = pods_v( $form_id, self::$secondary_submits, array(), true ); + + if ( empty( $secondary_submits ) ) { + return $button_input; + } + + if ( isset( $secondary_submits['action'] ) ) { + $secondary_submits = array( $secondary_submits ); + } + + $secondary_submits = array_reverse( $secondary_submits ); + + wp_enqueue_script( 'pods-gf' ); + wp_enqueue_style( 'pods-gf' ); + + $defaults = array( + 'imageUrl' => null, + 'text' => 'Alt Submit', + 'action' => 'alt', + 'value' => 1, + 'value_from_ui' => '', + 'cancel' => false, + ); + + foreach ( $secondary_submits as $secondary_submit ) { + $secondary_submit = array_merge( $defaults, $secondary_submit ); + + if ( ! empty( $secondary_submit['value_from_ui'] ) && class_exists( 'Pods_GF_UI' ) && ! empty( Pods_GF_UI::$pods_ui ) ) { + if ( in_array( $secondary_submit['value_from_ui'], array( 'next_id', 'prev_id' ), true ) ) { + // Setup data + Pods_GF_UI::$pods_ui->get_data(); + } + + if ( 'prev_id' == $secondary_submit['value_from_ui'] ) { + $secondary_submit['value'] = Pods_GF_UI::$pods_ui->pod->prev_id(); + } + elseif ( 'next_id' == $secondary_submit['value_from_ui'] ) { + $secondary_submit['value'] = Pods_GF_UI::$pods_ui->pod->next_id(); + } + + if ( in_array( $secondary_submit['value_from_ui'], array( 'next_id', 'prev_id' ), true ) ) { + // No ID, hide button + if ( empty( $secondary_submit['value'] ) ) { + continue; + } + } + } + + $secondary_action = sanitize_title( $secondary_submit['action'] ); + + $onclick = ' + if(window[\'gf_submitting_' . esc_attr( $form_id ) . '\']){return false;} + window[\'gf_submitting_' . esc_attr( $form_id ) . '\']=true; + '; + + if ( $secondary_submit['cancel'] ) { + $onclick .= ' + jQuery(\'#pods_gf_ui_action_' . esc_attr( $secondary_action ) . '\').val( this.value ); + jQuery(\'#pods_gf_secondary_' . esc_attr( $form_id ) . '\').submit(); + return false; + '; + } else { + $button_input .= ' + + '; + + $onclick = ' + jQuery(\'#pods_gf_ui_action_' . esc_attr( $secondary_action ) . '\').val( this.value ); + return true; + '; + } + + $onclick = preg_replace( '/\s+/', ' ', $onclick ); + + if ( empty( $secondary_submit['imageUrl'] ) ) { + if ( null !== $secondary_submit['value'] && $secondary_submit['text'] !== $secondary_submit['value'] ) { + $button_input .= ' '; + } + else { + $button_input .= ' '; + } + } + else { + $button_input .= ' '; + } + } + + return $button_input; + + } + + /** + * Add Secondary Submit button form. + * + * Warning: Gravity Forms Duplicate Prevention plugin's JS *will* break this! + * + * @param string $form_string Form HTML + * @param array $form GF Form array + * + * @return string Form HTML + */ + public static function gf_secondary_submit_form( $form_string, $form ) { + + $form_id = $form['id']; + + $secondary_submits = pods_v( $form_id, self::$secondary_submits, array(), true ); + + $anchor = GFFormDisplay::get_anchor( $form, false ); + $action = remove_query_arg( 'gf_token' ) . $anchor['id']; + + $secondary_submit_form = ' +
    + '; if ( ! empty( $secondary_submits ) ) { if ( isset( $secondary_submits['action'] ) ) { @@ -946,14 +1079,19 @@ public static function gf_secondary_submit_button( $button_input, $form ) { 'text' => 'Alt Submit', 'action' => 'alt', 'value' => 1, - 'value_from_ui' => '' + 'value_from_ui' => '', + 'cancel' => false, ); foreach ( $secondary_submits as $secondary_submit ) { $secondary_submit = array_merge( $defaults, $secondary_submit ); + if ( ! $secondary_submit['cancel'] ) { + continue; + } + if ( ! empty( $secondary_submit['value_from_ui'] ) && class_exists( 'Pods_GF_UI' ) && ! empty( Pods_GF_UI::$pods_ui ) ) { - if ( in_array( $secondary_submit['value_from_ui'], array( 'next_id', 'prev_id' ) ) ) { + if ( in_array( $secondary_submit['value_from_ui'], array( 'next_id', 'prev_id' ), true ) ) { // Setup data Pods_GF_UI::$pods_ui->get_data(); } @@ -965,7 +1103,7 @@ public static function gf_secondary_submit_button( $button_input, $form ) { $secondary_submit['value'] = Pods_GF_UI::$pods_ui->pod->next_id(); } - if ( in_array( $secondary_submit['value_from_ui'], array( 'next_id', 'prev_id' ) ) ) { + if ( in_array( $secondary_submit['value_from_ui'], array( 'next_id', 'prev_id' ), true ) ) { // No ID, hide button if ( empty( $secondary_submit['value'] ) ) { continue; @@ -973,32 +1111,29 @@ public static function gf_secondary_submit_button( $button_input, $form ) { } } - if ( empty( $secondary_submit['imageUrl'] ) ) { - if ( null !== $secondary_submit['value'] && $secondary_submit['text'] !== $secondary_submit['value'] ) { - $button_input .= ' '; - } - else { - $button_input .= ' '; - } - } - else { - $button_input .= ' '; - } + $secondary_action = sanitize_title( $secondary_submit['action'] ); + + $secondary_submit_form .= ' + + + '; } } - return $button_input; + $secondary_submit_form .= ''; + + $form_string .= $secondary_submit_form; + + return $form_string; } @@ -1464,6 +1599,14 @@ public function _gf_to_pods_handler( $form, $entry = array() ) { } } + if ( 1 === (int) pods_v( 'pods_gf_debug' ) && pods_is_admin() ) { + echo '
    ';
    +				var_dump( [ 'form_id' => $form['id'] ] );
    +				var_dump( compact( 'save_action', 'args', 'data' ) );
    +				echo '
    '; + die(); + } + if ( ! empty( $this->pod->pod_data ) ) { $id = call_user_func_array( array( $this->pod, $save_action ), $args ); @@ -1475,6 +1618,14 @@ public function _gf_to_pods_handler( $form, $entry = array() ) { do_action( 'pods_gf_to_pods_' . $this->pod->pod, $this->pod, $args, $save_action, $data, $id, $this ); } else { + if ( 1 === (int) pods_v( 'pods_gf_debug' ) && pods_is_admin() ) { + echo '
    ';
    +				var_dump( [ 'form_id' => $form['id'] ] );
    +				var_dump( compact( 'save_action', 'id', 'data' ) );
    +				echo '
    '; + die(); + } + $id = apply_filters( 'pods_gf_to_pod_' . $form['id'] . '_' . $save_action, $id, $this->pod, $data, $this ); $this->id = $id = apply_filters( 'pods_gf_to_pod_' . $save_action, $id, $this->pod, $data, $this ); @@ -2007,35 +2158,49 @@ public static function gf_dynamic_select( $form, $ajax = false, $dynamic_selects // Additional handling for showing an empty choice for fields that are not required. if ( empty( $field_obj->isRequired ) && empty( $field_obj->placeholder ) ) { if ( 'radio' === $field_obj->type || ( 'entry' !== rgget( 'view' ) && 'select' === $field_obj->type ) ) { - $needs_empty = true; - - // Check if we have an empty option already. - foreach ( $choices as $choice ) { - if ( '' === $choice['value'] ) { - $needs_empty = false; + /** + * Allow filtering whether to show the empty option for a dynamic select field. + * + * @param bool $show_empty_option Whether to show the empty option. + * @param GF_Field $field_obj The Gravity Forms field object. + */ + $show_empty_option = apply_filters( 'pods_gf_dynamic_select_show_empty_option', true, $field_obj ); + + if ( $show_empty_option ) { + $needs_empty = true; + + // Check if we have an empty option already. + foreach ( $choices as $choice ) { + if ( '' === $choice['value'] ) { + $needs_empty = false; - break; + break; + } } - } - if ( $needs_empty ) { - if ( ! empty( $dynamic_select['select_text'] ) ) { - $empty_text = $dynamic_select['select_text']; - } else { - $empty_text = __( 'Select One', 'pods-gravity-forms' ); + if ( $needs_empty ) { + if ( ! empty( $dynamic_select['select_text'] ) ) { + $empty_text = $dynamic_select['select_text']; + + if ( 'select' !== $field_obj->type ) { + $empty_text = trim( $empty_text, '-' ); + } + } else { + $empty_text = __( 'Select One', 'pods-gravity-forms' ); - if ( 'select' === $field_obj->type ) { - $empty_text = sprintf( '-- %s --', $empty_text ); + if ( 'select' === $field_obj->type ) { + $empty_text = sprintf( '-- %s --', $empty_text ); + } } - } - $empty_choice = array( - 'text' => $empty_text, - 'value' => '', - ); + $empty_choice = [ + 'text' => $empty_text, + 'value' => '', + ]; - // Add empty choice to front of choices list. - array_unshift( $choices, $empty_choice ); + // Add empty choice to front of choices list. + array_unshift( $choices, $empty_choice ); + } } } } @@ -2260,7 +2425,7 @@ public static function gf_prepopulate( $form, $ajax = false, $prepopulate = null foreach ( $gf_field->choices as $k => $choice ) { $gf_field->choices[ $k ]['isSelected'] = false; - if ( ( ! is_array( $value_override ) && $choice['value'] == $value_override ) || ( is_array( $value_override ) && in_array( $choice['value'], $value_override ) ) ) { + if ( ( ! is_array( $value_override ) && $choice['value'] == $value_override ) || ( is_array( $value_override ) && in_array( $choice['value'], $value_override, false ) ) ) { $gf_field->choices[ $k ]['isSelected'] = true; break; @@ -2283,8 +2448,32 @@ public static function gf_prepopulate( $form, $ajax = false, $prepopulate = null $pod_field_type = $pod->fields( $field_options['field'], 'type' ); } - if ( is_array( $pod ) && isset( $pod[ $field_options['field'] ] ) ) { - $value_override = $pod[ $field_options['field'] ]; + if ( is_array( $pod ) ) { + if ( isset( $pod[ $field_options['field'] ] ) ) { + $value_override = $pod[ $field_options['field'] ]; + } elseif ( 'checkbox' === $gf_field->type ) { + $checkbox_values = []; + + $choice_counter = 0; + + foreach ( $gf_field->choices as $choice ) { + $choice_counter ++; + + if ( ! isset( $pod[ $gf_field->id . '.' . $choice_counter ] ) ) { + continue; + } + + $checkbox_values[] = $pod[ $gf_field->id . '.' . $choice_counter ]; + } + + if ( ! empty( $checkbox_values ) ) { + if ( 1 === count( $checkbox_values ) ) { + $checkbox_values = reset( $checkbox_values ); + } + + $value_override = $checkbox_values; + } + } } if ( $pod_field_type ) { @@ -2424,7 +2613,7 @@ public static function gf_prepopulate( $form, $ajax = false, $prepopulate = null if ( 'boolean' === $pod_field_type && 1 === (int) $values && ! empty( $choice['value'] ) ) { $is_selected = true; } elseif ( ( ! is_array( $values ) && (string) $choice['value'] === (string) $values ) - || ( is_array( $values ) && in_array( $choice['value'], $values ) ) ) { + || ( is_array( $values ) && in_array( $choice['value'], $values, false ) ) ) { $is_selected = true; } @@ -2462,13 +2651,42 @@ public static function gf_prepopulate( $form, $ajax = false, $prepopulate = null if ( is_array( $value_override ) && 'list' === $gf_field->type ) { $choices = $gf_field->choices; - $value_override_chunked = array_chunk( $value_override, count( $choices ) ); + $total_choices = count( $choices ); - foreach ( $value_override_chunked as $k => $v ) { - $value_override_chunked[ $k ] = implode( '|', $v ); - } + // Check if the values are chunked already. + if ( isset( $value_override[0] ) && is_array( $value_override[0] ) && $total_choices === count( $value_override[0] ) ) { + foreach ( $value_override as $vo_key => $value_override_row ) { + // Replace pipes because GF has no workaround here. + // @todo If GF changes this, adjust our usage. + $value_override_row = array_map( static function ( $value ) { + return preg_replace( '/ {2,}/', ' ', str_replace( '|', ' ', $value ) ); + }, $value_override_row ); + + $value_override[ $vo_key ] = implode( '|', $value_override_row ); + } + + // Replace pipes because GF has no workaround here. + // @todo If GF changes this, adjust our usage. + $value_override = array_map( static function ( $value ) { + return preg_replace( '/ {2,}/', ' ', str_replace( ',', ' ', $value ) ); + }, $value_override ); + + $value_override = implode( ',', $value_override ); + } else { + $value_override_chunked = array_chunk( $value_override, $total_choices ); + + foreach ( $value_override_chunked as $k => $v ) { + $value_override_chunked[ $k ] = implode( '|', $v ); + } + + // Replace commas because GF has no workaround here. + // @todo If GF changes this, adjust our usage. + $value_override_chunked = array_map( static function ( $value ) { + return preg_replace( '/ {2,}/', ' ', str_replace( ',', ' ', $value ) ); + }, $value_override_chunked ); - $value_override = implode( ',', $value_override_chunked ); + $value_override = implode( ',', $value_override_chunked ); + } } $_GET[ 'pods_gf_field_' . $field ] = pods_slash( $value_override ); @@ -2480,8 +2698,39 @@ public static function gf_prepopulate( $form, $ajax = false, $prepopulate = null $post_value_override = apply_filters( 'pods_gf_field_value', $post_value_override, $value_override, $field, $field_options, $form, $prepopulate, $pod ); if ( null !== $post_value_override ) { - if ( is_array( $post_value_override ) && 'list' === $gf_field->type ) { - $post_value_override = maybe_serialize( $post_value_override ); + if ( 'list' === $gf_field->type ) { + if ( is_string( $post_value_override ) ) { + // Replace the commas with placeholders. + $post_value_override = str_replace( '\,', '__PODS_GF_COMMA__', $post_value_override ); + $post_value_override = explode( ',', $post_value_override ); + + foreach ( $post_value_override as $pvo_key => $post_value_override_row ) { + // Restore the placeholders with commas. + $post_value_override_row = str_replace( '__PODS_GF_COMMA__', ',', $post_value_override_row ); + + // Replace the pipes with placeholders. + $post_value_override_row = str_replace( '\|', '__PODS_GF_PIPE__', $post_value_override_row ); + + // Expand the row. + $post_value_override_row = explode( '|', $post_value_override_row ); + + // Restore the placeholders with pipes. + $post_value_override_row = array_map( static function ( $value ) { + return str_replace( '__PODS_GF_PIPE__', '|', $value ); + }, $post_value_override_row ); + + $post_value_override[ $pvo_key ] = $post_value_override_row; + } + } + + if ( is_array( $post_value_override ) ) { + if ( isset( $post_value_override[0] ) && is_array( $post_value_override[0] ) ) { + $post_value_override = array_merge( ...$post_value_override ); + } + + // @todo Figure out if the value needs to be serialized + //$post_value_override = maybe_serialize( $post_value_override ); + } } $_POST[ 'input_' . $field ] = pods_slash( $post_value_override ); @@ -2700,12 +2949,12 @@ public static function gf_markdown( $form, $ajax = false, $markdown = null ) { self::$actioned[$form['id']] = array(); } - self::$actioned[$form['id']][] = __FUNCTION__; - - if ( ! function_exists( 'Markdown' ) ) { - include_once PODS_GF_DIR . 'includes/Markdown.php'; + if ( ! class_exists( 'Pods_Gravity_Forms__Prefixed__Parsedown' ) ) { + return $form; } + self::$actioned[$form['id']][] = __FUNCTION__; + $sanitize_from_markdown = array( '-', '_' @@ -2731,8 +2980,11 @@ public static function gf_markdown( $form, $ajax = false, $markdown = null ) { $content = str_replace( $merge_tag, $merge_tag_sanitized, $content ); } + $parsedown = new Pods_Gravity_Forms__Prefixed__Parsedown(); + $parsedown->setSafeMode( true ); + // Run Markdown - $content = Markdown( $content ); + $content = $parsedown->text( $content ); // Unsanitize merge tags foreach ( $merge_tags as $merge_tag_match ) { @@ -2841,7 +3093,7 @@ public static function gf_read_only( $form, $ajax = false, $read_only = null ) { $field = $field_options['gf_field']; } - if ( is_array( $read_only['exclude_fields'] ) && ! empty( $read_only['exclude_fields'] ) && in_array( (string) $field, $read_only['exclude_fields'] ) ) { + if ( isset( $read_only['exclude_fields'] ) && is_array( $read_only['exclude_fields'] ) && ! empty( $read_only['exclude_fields'] ) && in_array( (string) $field, $read_only['exclude_fields'] ) ) { continue; } @@ -2937,7 +3189,7 @@ public static function gf_field_input_read_only( $input_html, $field, $value, $l return $input_html; } - $last_page = self::$actioned[$form_id][__FUNCTION__]; + $last_page = (int) self::$actioned[$form_id][__FUNCTION__]; $non_read_only = array( 'hidden', @@ -2956,10 +3208,10 @@ public static function gf_field_input_read_only( $input_html, $field, $value, $l $page_header = ''; - if ( isset( $field['pageNumber'] ) && 0 < $field['pageNumber'] && $last_page != $field['pageNumber'] ) { - self::$actioned[$form_id][__FUNCTION__] = $field['pageNumber']; + if ( isset( $field['pageNumber'] ) && 0 < (int) $field['pageNumber'] && $last_page !== (int) $field['pageNumber'] ) { + self::$actioned[$form_id][__FUNCTION__] = (int) $field['pageNumber']; - $page_header = '

    ' . $form['pagination']['pages'][( $field['pageNumber'] - 1 )] . '

    '; + $page_header = '

    ' . $form['pagination']['pages'][( (int) $field['pageNumber'] - 1 )] . '

    '; } if ( 'html' == $field_type ) { @@ -2992,7 +3244,7 @@ public static function gf_field_input_read_only( $input_html, $field, $value, $l $choice_number ++; } - if ( in_array( $choice['value'], $value ) || ( empty( $value ) && $choice['isSelected'] ) ) { + if ( in_array( $choice['value'], $value, false ) || ( empty( $value ) && $choice['isSelected'] ) ) { $values[$choice_number] = $choice['value']; $labels[] = $choice['text']; } @@ -3536,7 +3788,13 @@ public static function get_gf_field_value( $value, $params ) { $value = sprintf( '%s:%s:00', $value[0], $value[1] ); } } elseif ( in_array( $gf_field->type, array( 'list' ), true ) && is_array( $value ) && ! empty( $value ) ) { - $columns = array_keys( current( $value ) ); + $first_row = current( $value ); + + $columns = []; + + if ( is_array( $first_row ) ) { + $columns = array_keys( $first_row ); + } if ( $columns ) { $related_obj = false; @@ -4159,7 +4417,7 @@ public function setup_form( $form ) { // Markdown Syntax for HTML if ( isset( $this->options['markdown'] ) && ! empty( $this->options['markdown'] ) ) { - $form = self::gf_markdown( $form, $ajax, $this->options['markdown'] ); + $form = self::gf_markdown( $form, false, $this->options['markdown'] ); } // Submit Button customization @@ -4294,7 +4552,7 @@ public function _gf_after_submission( $entry, $form ) { return $entry; } - remove_action( 'gform_post_submission_' . $form['id'], array( $this, '_gf_after_submission' ), 10 ); + remove_action( 'gform_after_submission_' . $form['id'], array( $this, '_gf_after_submission' ) ); if ( isset( self::$actioned[$form['id']] ) && in_array( __FUNCTION__, self::$actioned[$form['id']] ) ) { return $entry; @@ -4352,7 +4610,8 @@ public function _gf_after_submission( $entry, $form ) { 'text' => 'Alt Submit', 'action' => 'alt', 'value' => 1, - 'value_from_ui' => '' + 'value_from_ui' => '', + 'cancel' => false, ); foreach ( $secondary_submits as $secondary_submit ) { diff --git a/includes/Pods_GF_Addon.php b/includes/Pods_GF_Addon.php index 52b23eb..5aff834 100755 --- a/includes/Pods_GF_Addon.php +++ b/includes/Pods_GF_Addon.php @@ -232,6 +232,7 @@ public function feed_settings_fields() { 'post_content_filtered', 'pinged', 'to_ping', + 'comments', ); $wp_object_fields = array(); @@ -347,7 +348,7 @@ public function feed_settings_fields() { // Add field names to labels $field_map['label'] = sprintf( - '%s
    (%s)', + '%s (%s)', esc_html( $field_map['label'] ), esc_html( $field_map['name'] ) ); @@ -801,7 +802,7 @@ public function populate_related_items_settings( $position, $form_id ) { %s %s', __( 'Populate Related Items from Pods', 'pods-gravity-forms' ), __( 'Check this box to populate the related items from Pods instead of keeping the list up-to-date manually.' ) ); + $tooltips['form_pods_populate_related_items_value'] = sprintf( '
    %s
    %s', __( 'Populate Related Items from Pods', 'pods-gravity-forms' ), __( 'Check this box to populate the related items from Pods instead of keeping the list up-to-date manually.' ) ); return $tooltips; @@ -901,6 +902,9 @@ public function process_feed( $feed, $entry, $form ) { try { $pods_gf->options['entry'] = $entry; + // Ensure other custom Pods GF submission handling does not duplicate. + remove_action( 'gform_after_submission_' . $form['id'], [ $pods_gf, '_gf_after_submission' ] ); + $id = $pods_gf->_gf_to_pods_handler( $form, $entry ); // Set post_id if we have it. @@ -1078,7 +1082,7 @@ public function _gf_pre_render( $form, $ajax = false, $entry = null, $admin_edit */ foreach ( $form['fields'] as $gf_field ) { if ( empty( $gf_field->pods_populate_related_items ) ) { - //continue; + continue; } $pod_field = null; @@ -1299,6 +1303,18 @@ public function setup_pods_gf( $form, $feed ) { */ $options = apply_filters( 'pods_gf_addon_options', $options, $feed['meta']['pod'], $form['id'], $feed, $form, $pod ); + /** + * Allow filtering of Pods GF options to set custom settings apart from Pods GF add-on options based on form ID. + * + * @param array $options Pods GF options. + * @param string $pod_name Pod name. + * @param int $form_id GF Form ID. + * @param array $feed GF Form feed array. + * @param array $form GF Form array. + * @param Pods $pod Pods object. + */ + $options = apply_filters( 'pods_gf_addon_options_' . $form['id'], $options, $feed['meta']['pod'], $form['id'], $feed, $form, $pod ); + $this->pods_gf[ $feed['id'] ] = pods_gf( $pod, $form['id'], $options ); $setup[ $form['id'] ] = true; diff --git a/includes/Pods_GF_UI.php b/includes/Pods_GF_UI.php index e86b96b..d5b1b85 100755 --- a/includes/Pods_GF_UI.php +++ b/includes/Pods_GF_UI.php @@ -490,7 +490,7 @@ private function setup_ui() { if ( 0 < $id ) { $lead = GFAPI::get_entry( $id ); - if ( !empty( $lead ) && ! is_wp_error( $lead ) ) { + if ( !empty( $lead ) && ! is_wp_error( $lead ) && 'active' === $lead['status'] ) { $this->pod = array( $lead[ 'id' ] => $lead ); @@ -522,7 +522,9 @@ private function setup_ui() { $total_found = count( $this->pod ); } else { - $search_criteria = apply_filters( 'pods_gf_ui_search_criteria', array() ); + $search_criteria = apply_filters( 'pods_gf_ui_search_criteria', array( + 'status' => 'active', + ) ); $sorting = apply_filters( 'pods_gf_ui_sorting', null ); $paging = array( 'offset' => 0, @@ -965,7 +967,7 @@ public function _action_view( $obj = null ) { return; } ?> -
    +
    icon ) { ?> style="background-position:0 0;background-size:100%;background-image:url(icon; ?>);">

    Forms) -* Added: Ability to map Address and List fields -* Added: Ability to map Category and Post Tag fields -* Added: Ability to map sub fields to a pod field (Name [First Name], Address [Street Line 1], etc) -* Fixed: Ensure time fields get mapped correctly (props @mmarvin1) -* Fixed: Ensure default pods-gf-ui shortcode is only added/run on content within the loop (props @jamesgol) -* Fixed: Empty id used for Pods GF UI -* Fixed: Callback handling for Pods GF UI -* Fixed: Default Post Author mapping - -= 1.2 - October 4th, 2016 = - -* Added: When using a custom action and setting the form ID option in Pods GF UI, a new custom action will be used which embeds the GF form (if no callback provided in action_data option) -* Added: New Pods GF UI option, specific to each action, for `action_link` which corresponds to the `action_links` Pods UI option -* Fixed: Support for recent GF versions where pre_save_id hook uses a different Form-specific naming convention -* Fixed: Custom confirmation handling may have not been functioning properly in some cases -* Fixed: Removed some issues that were causing PHP notices - - -= 1.1 - June 13th, 2016 = - -* Added: Support for edit mode when using the Pods GF add-on mapping in the GF UI -- Use the new filter `pods_gf_addon_edit_id`, just return the ID to edit and the options will automatically be set for you -* Added: When filtering the Pods data in `Pods_GF::gf_to_pods()` (via the `pods_gf_to_pods_data` and related filters), if you set the proper ID field in that array it will now be used to *save* over the existing item; Helpful for dynamic editing configurations based upon different processes and workflows in the code -* Added: `Pods_GF::confirmation()` now supports `{@gf_to_pods_id}` replacement in confirmation URLs, replacing the variable properly to the resulting saved ID -* Fixed: `Pods_GF::_gf_to_pods_handler()` would sometimes get the action improperly set to `edit`, but only `add`, `save`, or `bypass` are valid -* Fixed: When an invalid pod is called in `Pods_GF::_gf_to_pods_handler()`, there's now a proper fallback to avoid PHP errors/warnings/notices -* Fixed: When an invalid pod is called in `Pods_GF::_gf_field_validation()`, there's now a proper fallback to avoid PHP errors/warnings/notices -* Fixed: `Pods_GF::confirmation()` would add the `gform_confirmation_{$form_id}` filter incorrectly and would cause PHP warnings about the callback, causing the confirmation functionality to not work properly -* Fixed: `Pods_GF::confirmation()` confirmation URL replacement now handles a few more cases where previously PHP notices would result -* Changed: `Pods_GF` is now storing multiple instances statically, cannot be called with `new Pods_GF()`, must be called with `Pods_GF::get_instance()` but more importantly should be called through the standard `pods_gf()` helper function to remain backwards compatible with previous versions -* Changed: `Pods_GF::$gf_to_pods_id` is no longer an integer, but an array of integers keyed by the GF Form ID -* Changed: `Pods_GF::$keep_files` is no longer an boolean, but an array of booleans keyed by the GF Form ID - -= 1.0 - March 4th, 2016 = - -* Initial release diff --git a/ui/pods-gf.js b/ui/pods-gf.js index 56777b7..79cb68e 100644 --- a/ui/pods-gf.js +++ b/ui/pods-gf.js @@ -101,4 +101,9 @@ jQuery( function() { } } ); } + + // Set target page number to zero if we are in a view-only form. + if ( jQuery( '.pods-ui.pods-gf-ui-view-only' )[ 0 ] && 1 < jQuery( '.gform_page' ).length ) { + jQuery( '.pods-ui.pods-gf-ui-view-only' ).find( 'input[id^="gform_target_page_number_"]' ).val( 0 ); + } } ); \ No newline at end of file diff --git a/vendor-prefixed/autoload-classmap.php b/vendor-prefixed/autoload-classmap.php new file mode 100644 index 0000000..b1b14e9 --- /dev/null +++ b/vendor-prefixed/autoload-classmap.php @@ -0,0 +1,9 @@ + $strauss_src . '/erusev/parsedown/Parsedown.php', +); \ No newline at end of file diff --git a/vendor-prefixed/autoload.php b/vendor-prefixed/autoload.php new file mode 100644 index 0000000..35b2b2f --- /dev/null +++ b/vendor-prefixed/autoload.php @@ -0,0 +1,20 @@ +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); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + $Blocks []= $CurrentBlock; + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) + { + $CurrentBlock['element']['text'] .= "\n".$text; + } + else + { + $Blocks []= $CurrentBlock; + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + + # ~ + + $Blocks []= $CurrentBlock; + + unset($Blocks[0]); + + # ~ + + $markup = ''; + + foreach ($Blocks as $Block) + { + if (isset($Block['hidden'])) + { + continue; + } + + $markup .= "\n"; + $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); + } + + $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). + * + @license MIT + Modified by Pods Framework Team on 29-March-2024 using {@see https://github.com/BrianHenryIE/strauss}. +*/ + $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, '|'); + + $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).'"'; + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + if (isset($text)) + { + $markup .= '>'; + + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']); + } + elseif (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + + $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/vendor-prefixed/erusev/parsedown/README.md b/vendor-prefixed/erusev/parsedown/README.md new file mode 100644 index 0000000..b5d9ed2 --- /dev/null +++ b/vendor-prefixed/erusev/parsedown/README.md @@ -0,0 +1,86 @@ +> I also make [Caret](https://caret.io?ref=parsedown) - a Markdown editor for Mac and PC. + +## Parsedown + +[![Build Status](https://img.shields.io/travis/erusev/parsedown/master.svg?style=flat-square)](https://travis-ci.org/erusev/parsedown) + + +Better Markdown Parser in PHP + +[Demo](http://parsedown.org/demo) | +[Benchmarks](http://parsedown.org/speed) | +[Tests](http://parsedown.org/tests/) | +[Documentation](https://github.com/erusev/parsedown/wiki/) + +### Features + +* One File +* No Dependencies +* Super Fast +* Extensible +* [GitHub flavored](https://help.github.com/articles/github-flavored-markdown) +* Tested in 5.3 to 7.1 and in HHVM +* [Markdown Extra extension](https://github.com/erusev/parsedown-extra) + +### Installation + +Include `Parsedown.php` or install [the composer package](https://packagist.org/packages/erusev/parsedown). + +### Example + +``` php +$Parsedown = new Parsedown(); + +echo $Parsedown->text('Hello _Parsedown_!'); # prints:

    Hello Parsedown!

    +``` + +More examples in [the wiki](https://github.com/erusev/parsedown/wiki/) and in [this video tutorial](http://youtu.be/wYZBY8DEikI). + +### Security + +Parsedown is capable of escaping user-input within the HTML that it generates. Additionally Parsedown will apply sanitisation to additional scripting vectors (such as scripting link destinations) that are introduced by the markdown syntax itself. + +To tell Parsedown that it is processing untrusted user-input, use the following: +```php +$parsedown = new Parsedown; +$parsedown->setSafeMode(true); +``` + +If instead, you wish to allow HTML within untrusted user-input, but still want output to be free from XSS it is recommended that you make use of a HTML sanitiser that allows HTML tags to be whitelisted, like [HTML Purifier](http://htmlpurifier.org/). + +In both cases you should strongly consider employing defence-in-depth measures, like [deploying a Content-Security-Policy](https://scotthelme.co.uk/content-security-policy-an-introduction/) (a browser security feature) so that your page is likely to be safe even if an attacker finds a vulnerability in one of the first lines of defence above. + +#### Security of Parsedown Extensions + +Safe mode does not necessarily yield safe results when using extensions to Parsedown. Extensions should be evaluated on their own to determine their specific safety against XSS. + +### Escaping HTML +> ⚠️  **WARNING:** This method isn't safe from XSS! + +If you wish to escape HTML **in trusted input**, you can use the following: +```php +$parsedown = new Parsedown; +$parsedown->setMarkupEscaped(true); +``` + +Beware that this still allows users to insert unsafe scripting vectors, such as links like `[xss](javascript:alert%281%29)`. + +### Questions + +**How does Parsedown work?** + +It tries to read Markdown like a human. First, it looks at the lines. It’s interested in how the lines start. This helps it recognise blocks. It knows, for example, that if a line starts with a `-` then perhaps it belongs to a list. Once it recognises the blocks, it continues to the content. As it reads, it watches out for special characters. This helps it recognise inline elements (or inlines). + +We call this approach "line based". We believe that Parsedown is the first Markdown parser to use it. Since the release of Parsedown, other developers have used the same approach to develop other Markdown parsers in PHP and in other languages. + +**Is it compliant with CommonMark?** + +It passes most of the CommonMark tests. Most of the tests that don't pass deal with cases that are quite uncommon. Still, as CommonMark matures, compliance should improve. + +**Who uses it?** + +[Laravel Framework](https://laravel.com/), [Bolt CMS](http://bolt.cm/), [Grav CMS](http://getgrav.org/), [Herbie CMS](http://www.getherbie.org/), [Kirby CMS](http://getkirby.com/), [October CMS](http://octobercms.com/), [Pico CMS](http://picocms.org), [Statamic CMS](http://www.statamic.com/), [phpDocumentor](http://www.phpdoc.org/), [RaspberryPi.org](http://www.raspberrypi.org/), [Symfony demo](https://github.com/symfony/symfony-demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents). + +**How can I help?** + +Use it, star it, share it and if you feel generous, [donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=528P3NZQMP8N2). diff --git a/vendor-prefixed/erusev/parsedown/composer.json b/vendor-prefixed/erusev/parsedown/composer.json new file mode 100644 index 0000000..f8b40f8 --- /dev/null +++ b/vendor-prefixed/erusev/parsedown/composer.json @@ -0,0 +1,33 @@ +{ + "name": "erusev/parsedown", + "description": "Parser for Markdown.", + "keywords": ["markdown", "parser"], + "homepage": "http://parsedown.org", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "require": { + "php": ">=5.3.0", + "ext-mbstring": "*" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "autoload": { + "psr-0": {"Parsedown": ""} + }, + "autoload-dev": { + "psr-0": { + "TestParsedown": "test/", + "ParsedownTest": "test/", + "CommonMarkTest": "test/", + "CommonMarkTestWeak": "test/" + } + } +}