├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── LICENSE
├── composer.json
└── src
├── Bundle.php
├── Bundles
├── Fatdown.php
├── Fatdown
│ └── Renderer.php
├── Forum.php
├── Forum
│ └── Renderer.php
├── MediaPack.php
└── MediaPack
│ └── Renderer.php
├── Configurator.php
├── Configurator
├── Bundle.php
├── BundleGenerator.php
├── Bundles
│ ├── Fatdown.php
│ ├── Forum.php
│ └── MediaPack.php
├── Collections
│ ├── AttributeCollection.php
│ ├── AttributeFilterChain.php
│ ├── AttributeFilterCollection.php
│ ├── AttributeList.php
│ ├── AttributePreprocessorCollection.php
│ ├── Collection.php
│ ├── FilterChain.php
│ ├── HostnameList.php
│ ├── MinifierList.php
│ ├── NormalizedCollection.php
│ ├── NormalizedList.php
│ ├── PluginCollection.php
│ ├── RulesGeneratorList.php
│ ├── Ruleset.php
│ ├── SchemeList.php
│ ├── TagCollection.php
│ ├── TagFilterChain.php
│ ├── TagList.php
│ ├── TemplateCheckList.php
│ ├── TemplateNormalizationList.php
│ └── TemplateParameterCollection.php
├── ConfigProvider.php
├── Exceptions
│ └── UnsafeTemplateException.php
├── FilterableConfigValue.php
├── Helpers
│ ├── AVTHelper.php
│ ├── ConfigHelper.php
│ ├── ContextSafeness.php
│ ├── ElementInspector.php
│ ├── FilterHelper.php
│ ├── FilterSyntaxMatcher.php
│ ├── NodeLocator.php
│ ├── RegexpBuilder.php
│ ├── RegexpParser.php
│ ├── RulesHelper.php
│ ├── TemplateHelper.php
│ ├── TemplateInspector.php
│ ├── TemplateLoader.php
│ ├── TemplateModifier.php
│ ├── TemplateParser.php
│ ├── TemplateParser
│ │ ├── IRProcessor.php
│ │ ├── Normalizer.php
│ │ ├── Optimizer.php
│ │ └── Parser.php
│ └── XPathHelper.php
├── Items
│ ├── Attribute.php
│ ├── AttributeFilter.php
│ ├── AttributeFilters
│ │ ├── AbstractMapFilter.php
│ │ ├── AlnumFilter.php
│ │ ├── ChoiceFilter.php
│ │ ├── ColorFilter.php
│ │ ├── EmailFilter.php
│ │ ├── FalseFilter.php
│ │ ├── FloatFilter.php
│ │ ├── FontfamilyFilter.php
│ │ ├── HashmapFilter.php
│ │ ├── IdentifierFilter.php
│ │ ├── IntFilter.php
│ │ ├── IpFilter.php
│ │ ├── IpportFilter.php
│ │ ├── Ipv4Filter.php
│ │ ├── Ipv6Filter.php
│ │ ├── MapFilter.php
│ │ ├── NumberFilter.php
│ │ ├── RangeFilter.php
│ │ ├── RegexpFilter.php
│ │ ├── SimpletextFilter.php
│ │ ├── TimestampFilter.php
│ │ ├── UintFilter.php
│ │ └── UrlFilter.php
│ ├── AttributePreprocessor.php
│ ├── Filter.php
│ ├── ProgrammableCallback.php
│ ├── Regexp.php
│ ├── Tag.php
│ ├── TagFilter.php
│ ├── Template.php
│ ├── TemplateDocument.php
│ └── UnsafeTemplate.php
├── JavaScript.php
├── JavaScript
│ ├── CallbackGenerator.php
│ ├── Code.php
│ ├── ConfigOptimizer.php
│ ├── ConfigValue.php
│ ├── Dictionary.php
│ ├── Encoder.php
│ ├── FunctionCache.php
│ ├── FunctionProvider.php
│ ├── Hasher.php
│ ├── HintGenerator.php
│ ├── Minifier.php
│ ├── Minifiers
│ │ ├── ClosureCompilerApplication.php
│ │ ├── ClosureCompilerService.php
│ │ ├── FirstAvailable.php
│ │ ├── MatthiasMullieMinify.php
│ │ └── Noop.php
│ ├── OnlineMinifier.php
│ ├── RegexpConvertor.php
│ ├── StylesheetCompressor.php
│ ├── externs.application.js
│ └── externs.service.js
├── RecursiveParser.php
├── RecursiveParser
│ ├── AbstractRecursiveMatcher.php
│ ├── CachingRecursiveParser.php
│ └── MatcherInterface.php
├── RendererGenerator.php
├── RendererGenerators
│ ├── PHP.php
│ ├── PHP
│ │ ├── AbstractOptimizer.php
│ │ ├── BranchOutputOptimizer.php
│ │ ├── ControlStructuresOptimizer.php
│ │ ├── Optimizer.php
│ │ ├── Quick.php
│ │ ├── Serializer.php
│ │ ├── SwitchStatement.php
│ │ ├── XPathConvertor.php
│ │ └── XPathConvertor
│ │ │ └── Convertors
│ │ │ ├── AbstractConvertor.php
│ │ │ ├── BooleanFunctions.php
│ │ │ ├── BooleanOperators.php
│ │ │ ├── Comparisons.php
│ │ │ ├── Core.php
│ │ │ ├── Math.php
│ │ │ ├── MultiByteStringManipulation.php
│ │ │ ├── PHP80Functions.php
│ │ │ ├── SingleByteStringFunctions.php
│ │ │ └── SingleByteStringManipulation.php
│ ├── Unformatted.php
│ └── XSLT.php
├── Rendering.php
├── RulesGenerator.php
├── RulesGenerators
│ ├── AllowAll.php
│ ├── AutoCloseIfVoid.php
│ ├── AutoReopenFormattingElements.php
│ ├── BlockElementsCloseFormattingElements.php
│ ├── BlockElementsFosterFormattingElements.php
│ ├── DisableAutoLineBreaksIfNewLinesArePreserved.php
│ ├── EnforceContentModels.php
│ ├── EnforceOptionalEndTags.php
│ ├── IgnoreTagsInCode.php
│ ├── IgnoreTextIfDisallowed.php
│ ├── IgnoreWhitespaceAroundBlockElements.php
│ ├── Interfaces
│ │ ├── BooleanRulesGenerator.php
│ │ └── TargetedRulesGenerator.php
│ ├── ManageParagraphs.php
│ └── TrimFirstLineInCodeBlocks.php
├── TemplateCheck.php
├── TemplateChecker.php
├── TemplateChecks
│ ├── AbstractDynamicContentCheck.php
│ ├── AbstractFlashRestriction.php
│ ├── AbstractXSLSupportCheck.php
│ ├── DisallowAttributeSets.php
│ ├── DisallowCopy.php
│ ├── DisallowDisableOutputEscaping.php
│ ├── DisallowDynamicAttributeNames.php
│ ├── DisallowDynamicElementNames.php
│ ├── DisallowElement.php
│ ├── DisallowElementNS.php
│ ├── DisallowFlashFullScreen.php
│ ├── DisallowNodeByXPath.php
│ ├── DisallowObjectParamsWithGeneratedName.php
│ ├── DisallowPHPTags.php
│ ├── DisallowUncompilableXSL.php
│ ├── DisallowUnsafeCopyOf.php
│ ├── DisallowUnsafeDynamicCSS.php
│ ├── DisallowUnsafeDynamicJS.php
│ ├── DisallowUnsafeDynamicURL.php
│ ├── DisallowUnsupportedXSL.php
│ ├── DisallowXPathFunction.php
│ ├── RestrictFlashNetworking.php
│ └── RestrictFlashScriptAccess.php
├── TemplateNormalizations
│ ├── AbstractChooseOptimization.php
│ ├── AbstractConstantFolding.php
│ ├── AbstractNormalization.php
│ ├── AddAttributeValueToElements.php
│ ├── ConvertCurlyExpressionsInText.php
│ ├── Custom.php
│ ├── DeoptimizeIf.php
│ ├── EnforceHTMLOmittedEndTags.php
│ ├── FixUnescapedCurlyBracesInHtmlAttributes.php
│ ├── FoldArithmeticConstants.php
│ ├── FoldConstantXPathExpressions.php
│ ├── InlineAttributes.php
│ ├── InlineCDATA.php
│ ├── InlineElements.php
│ ├── InlineInferredValues.php
│ ├── InlineTextElements.php
│ ├── InlineXPathLiterals.php
│ ├── MergeConsecutiveCopyOf.php
│ ├── MergeIdenticalConditionalBranches.php
│ ├── MinifyInlineCSS.php
│ ├── MinifyXPathExpressions.php
│ ├── NormalizeAttributeNames.php
│ ├── NormalizeElementNames.php
│ ├── NormalizeUrls.php
│ ├── OptimizeChoose.php
│ ├── OptimizeChooseAttributes.php
│ ├── OptimizeChooseDeadBranches.php
│ ├── OptimizeChooseText.php
│ ├── OptimizeConditionalAttributes.php
│ ├── OptimizeConditionalValueOf.php
│ ├── OptimizeNestedConditionals.php
│ ├── PreserveSingleSpaces.php
│ ├── RemoveComments.php
│ ├── RemoveInterElementWhitespace.php
│ ├── RemoveLivePreviewAttributes.php
│ ├── RenameLivePreviewEvent.php
│ ├── SetAttributeOnElements.php
│ ├── SetRelNoreferrerOnTargetedLinks.php
│ ├── SortAttributesByName.php
│ ├── TransposeComments.php
│ └── UninlineAttributes.php
├── TemplateNormalizer.php
├── Traits
│ ├── CollectionProxy.php
│ ├── Configurable.php
│ └── TemplateSafeness.php
├── UrlConfig.php
└── Validators
│ ├── AttributeName.php
│ ├── TagName.php
│ └── TemplateParameterName.php
├── Parser.js
├── Parser.php
├── Parser
├── AttributeFilters
│ ├── EmailFilter.js
│ ├── EmailFilter.php
│ ├── FalseFilter.js
│ ├── FalseFilter.php
│ ├── HashmapFilter.js
│ ├── HashmapFilter.php
│ ├── MapFilter.js
│ ├── MapFilter.php
│ ├── NetworkFilter.js
│ ├── NetworkFilter.php
│ ├── NumericFilter.js
│ ├── NumericFilter.php
│ ├── RegexpFilter.js
│ ├── RegexpFilter.php
│ ├── TimestampFilter.js
│ ├── TimestampFilter.php
│ ├── UrlFilter.js
│ └── UrlFilter.php
├── FilterProcessing.js
├── FilterProcessing.php
├── Logger.js
├── Logger.php
├── NullLogger.js
├── Tag.js
├── Tag.php
└── utils.js
├── Plugins
├── AbstractStaticUrlReplacer
│ ├── AbstractConfigurator.php
│ ├── AbstractParser.php
│ └── Parser.js
├── Autoemail
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── Autoimage
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── Autolink
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── Autovideo
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── BBCodes
│ ├── Configurator.php
│ ├── Configurator
│ │ ├── BBCode.php
│ │ ├── BBCodeCollection.php
│ │ ├── BBCodeMonkey.php
│ │ ├── Repository.php
│ │ ├── RepositoryCollection.php
│ │ └── repository.xml
│ ├── Parser.js
│ └── Parser.php
├── Censor
│ ├── Configurator.php
│ ├── Helper.php
│ ├── Parser.js
│ └── Parser.php
├── ConfiguratorBase.php
├── Emoji
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── Emoticons
│ ├── Configurator.php
│ ├── Configurator
│ │ └── EmoticonCollection.php
│ ├── Parser.js
│ └── Parser.php
├── Escaper
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── FancyPants
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── HTMLComments
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── HTMLElements
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── HTMLEntities
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── Keywords
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── Litedown
│ ├── Configurator.php
│ ├── Parser.php
│ └── Parser
│ │ ├── LinkAttributesSetter.js
│ │ ├── LinkAttributesSetter.php
│ │ ├── ParsedText.js
│ │ ├── ParsedText.php
│ │ ├── Passes
│ │ ├── AbstractInlineMarkup.js
│ │ ├── AbstractInlineMarkup.php
│ │ ├── AbstractPass.php
│ │ ├── AbstractScript.js
│ │ ├── AbstractScript.php
│ │ ├── Blocks.js
│ │ ├── Blocks.php
│ │ ├── Emphasis.js
│ │ ├── Emphasis.php
│ │ ├── ForcedLineBreaks.js
│ │ ├── ForcedLineBreaks.php
│ │ ├── Images.js
│ │ ├── Images.php
│ │ ├── InlineCode.js
│ │ ├── InlineCode.php
│ │ ├── InlineSpoiler.js
│ │ ├── InlineSpoiler.php
│ │ ├── LinkReferences.js
│ │ ├── LinkReferences.php
│ │ ├── Links.js
│ │ ├── Links.php
│ │ ├── Strikethrough.js
│ │ ├── Strikethrough.php
│ │ ├── Subscript.js
│ │ ├── Subscript.php
│ │ ├── Superscript.js
│ │ └── Superscript.php
│ │ ├── Slugger.js
│ │ └── Slugger.php
├── MediaEmbed
│ ├── Configurator.php
│ ├── Configurator
│ │ ├── AbstractConfigurableHostHelper.php
│ │ ├── Collections
│ │ │ ├── CachedDefinitionCollection.php
│ │ │ ├── SiteDefinitionCollection.php
│ │ │ └── XmlFileDefinitionCollection.php
│ │ ├── MastodonHelper.php
│ │ ├── SiteHelpers
│ │ │ ├── AbstractConfigurableHostHelper.php
│ │ │ ├── AbstractSiteHelper.php
│ │ │ ├── BlueskyHelper.php
│ │ │ ├── MastodonHelper.php
│ │ │ └── XenForoHelper.php
│ │ ├── TemplateBuilder.php
│ │ ├── TemplateGenerator.php
│ │ ├── TemplateGenerators
│ │ │ ├── Choose.php
│ │ │ ├── Flash.php
│ │ │ └── Iframe.php
│ │ └── XenForoHelper.php
│ ├── Parser.js
│ ├── Parser.php
│ └── Parser
│ │ └── tagFilter.js
├── ParserBase.php
├── PipeTables
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
├── Preg
│ ├── Configurator.php
│ ├── Parser.js
│ └── Parser.php
└── TaskLists
│ ├── Configurator.php
│ ├── Helper.php
│ └── filterListItem.js
├── Renderer.php
├── Renderers
├── PHP.php
├── Unformatted.php
└── XSLT.php
├── Unparser.php
├── Utils.php
├── Utils
├── Http.php
├── Http
│ ├── Client.php
│ └── Clients
│ │ ├── Cached.php
│ │ ├── Curl.php
│ │ └── Native.php
├── ParsedDOM.php
├── ParsedDOM
│ ├── Document.php
│ └── Element.php
└── XPath.php
└── render.js
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | outdated:
11 | runs-on: ubuntu-latest
12 | container: setupphp/node@sha256:9271c0a914deb70c1717ec113410c9d43e48123d0ed398bb696f00f4f0ef15ba
13 |
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | php-version:
18 | - "8.1"
19 |
20 | steps:
21 | - uses: "actions/checkout@v4"
22 | - uses: "shivammathur/setup-php@v2"
23 | with:
24 | php-version: ${{ matrix.php-version }}
25 |
26 | - name: Cache Composer packages
27 | id: composer-cache
28 | uses: actions/cache@v4
29 | with:
30 | path: vendor
31 | key: php-outdated
32 |
33 | - name: Install Composer dependencies
34 | run: composer install --prefer-dist --no-progress
35 |
36 | - name: Run test suite
37 | run: composer run-script test
38 |
39 | current:
40 | runs-on: ubuntu-latest
41 | strategy:
42 | matrix:
43 | php-version:
44 | - "8.1"
45 | - "8.2"
46 | - "8.3"
47 | - "8.4"
48 |
49 | steps:
50 | - uses: "actions/checkout@v4"
51 | - uses: "shivammathur/setup-php@v2"
52 | with:
53 | php-version: ${{ matrix.php-version }}
54 |
55 | - name: Validate composer.json and composer.lock
56 | run: composer validate --strict
57 |
58 | - name: Cache Composer packages
59 | id: composer-cache
60 | uses: actions/cache@v4
61 | with:
62 | path: vendor
63 | key: php-${{ matrix.php-version }}
64 |
65 | - name: Install Composer dependencies
66 | run: composer install --prefer-dist --no-progress
67 |
68 | - name: Run test suite
69 | run: composer run-script test
70 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.*
2 | /composer.lock
3 | /scripts/composer.lock
4 | /scripts/vendor
5 | /tests/.cache
6 | /vendor
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) The s9e authors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "s9e/text-formatter",
3 | "type": "library",
4 | "description": "Multi-purpose text formatting and markup library. Plugins offer support for BBCodes, Markdown, emoticons, HTML, embedding third-party media (YouTube, etc...), enhanced typography and more.",
5 | "homepage": "https://github.com/s9e/TextFormatter/",
6 | "keywords": ["bbcode","bbcodes","blog","censor","embed","emoji","emoticons","engine","forum","html","markdown","markup","media","parser","shortcodes"],
7 | "license": "MIT",
8 | "require": {
9 | "php": "^8.1",
10 | "ext-dom": "*",
11 | "ext-filter": "*",
12 | "lib-pcre": ">=8.13",
13 |
14 | "s9e/regexp-builder": "^1.4",
15 | "s9e/sweetdom": "^3.4"
16 | },
17 | "require-dev": {
18 | "code-lts/doctum": "*",
19 | "matthiasmullie/minify": "*",
20 | "phpunit/phpunit": "^9.5",
21 | "friendsofphp/php-cs-fixer": "^3.52"
22 | },
23 | "suggest": {
24 | "ext-curl": "Improves the performance of the MediaEmbed plugin and some JavaScript minifiers",
25 | "ext-intl": "Allows international URLs to be accepted by the URL filter",
26 | "ext-json": "Enables the generation of a JavaScript parser",
27 | "ext-mbstring": "Improves the performance of the PHP renderer",
28 | "ext-tokenizer": "Improves the performance of the PHP renderer",
29 | "ext-xsl": "Enables the XSLT renderer",
30 | "ext-zlib": "Enables gzip compression when scraping content via the MediaEmbed plugin"
31 | },
32 | "autoload": {
33 | "psr-4": {
34 | "s9e\\TextFormatter\\": "src"
35 | }
36 | },
37 | "autoload-dev": {
38 | "psr-4": {
39 | "s9e\\TextFormatter\\Tests\\": "tests"
40 | }
41 | },
42 | "scripts": {
43 | "post-update-cmd": "php scripts/patchReadme.php",
44 | "test": "phpunit --exclude-group ''"
45 | },
46 | "extra": {
47 | "version": "2.19.1-dev"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Configurator/Bundle.php:
--------------------------------------------------------------------------------
1 | configure($configurator);
33 |
34 | return $configurator;
35 | }
36 |
37 | /**
38 | * Return extra options to be passed to the bundle generator
39 | *
40 | * Used by scripts/generateBundles.php
41 | *
42 | * @return array
43 | */
44 | public static function getOptions()
45 | {
46 | return [];
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Configurator/Bundles/MediaPack.php:
--------------------------------------------------------------------------------
1 | MediaEmbed))
22 | {
23 | // Only create BBCodes if the BBCodes plugin is already loaded
24 | $pluginOptions = ['createMediaBBCode' => isset($configurator->BBCodes)];
25 |
26 | $configurator->plugins->load('MediaEmbed', $pluginOptions);
27 | }
28 |
29 | foreach ($configurator->MediaEmbed->defaultSites as $siteId => $siteConfig)
30 | {
31 | $configurator->MediaEmbed->add($siteId);
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/Configurator/Collections/AttributeCollection.php:
--------------------------------------------------------------------------------
1 | items);
35 | sort($list);
36 |
37 | return $list;
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Configurator/Collections/MinifierList.php:
--------------------------------------------------------------------------------
1 | getMinifierInstance($minifier);
27 | }
28 | elseif (is_array($minifier) && !empty($minifier[0]))
29 | {
30 | $minifier = $this->getMinifierInstance($minifier[0], array_slice($minifier, 1));
31 | }
32 |
33 | if (!($minifier instanceof Minifier))
34 | {
35 | throw new InvalidArgumentException('Invalid minifier ' . var_export($minifier, true));
36 | }
37 |
38 | return $minifier;
39 | }
40 |
41 | /**
42 | * Create and return a Minifier instance
43 | *
44 | * @param string $name Minifier's name
45 | * @param array $args Constructor's arguments
46 | * @return Minifier
47 | */
48 | protected function getMinifierInstance($name, array $args = [])
49 | {
50 | $className = 's9e\\TextFormatter\\Configurator\\JavaScript\\Minifiers\\' . $name;
51 | if (!class_exists($className))
52 | {
53 | throw new InvalidArgumentException('Invalid minifier ' . var_export($name, true));
54 | }
55 |
56 | $reflection = new ReflectionClass($className);
57 | $minifier = (empty($args)) ? $reflection->newInstance() : $reflection->newInstanceArgs($args);
58 |
59 | return $minifier;
60 | }
61 | }
--------------------------------------------------------------------------------
/src/Configurator/Collections/RulesGeneratorList.php:
--------------------------------------------------------------------------------
1 | normalizeValue($value));
52 |
53 | $cnt = 0;
54 | foreach ($this->items as $i => $rulesGenerator)
55 | {
56 | if ($rulesGenerator instanceof $className)
57 | {
58 | ++$cnt;
59 | unset($this->items[$i]);
60 | }
61 | }
62 | $this->items = array_values($this->items);
63 |
64 | return $cnt;
65 | }
66 |
67 | return parent::remove($value);
68 | }
69 | }
--------------------------------------------------------------------------------
/src/Configurator/Collections/SchemeList.php:
--------------------------------------------------------------------------------
1 | items) . '$/Di');
24 | }
25 |
26 | /**
27 | * Validate and normalize a scheme name to lowercase, or throw an exception if invalid
28 | *
29 | * @link http://tools.ietf.org/html/rfc3986#section-3.1
30 | *
31 | * @param string $scheme URL scheme, e.g. "file" or "ed2k"
32 | * @return string
33 | */
34 | public function normalizeValue($scheme)
35 | {
36 | if (!preg_match('#^[a-z][a-z0-9+\\-.]*$#Di', $scheme))
37 | {
38 | throw new InvalidArgumentException("Invalid scheme name '" . $scheme . "'");
39 | }
40 |
41 | return strtolower($scheme);
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Configurator/Collections/TagCollection.php:
--------------------------------------------------------------------------------
1 | items);
34 | sort($list);
35 |
36 | return $list;
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Configurator/Collections/TemplateCheckList.php:
--------------------------------------------------------------------------------
1 | node = $node;
29 | }
30 |
31 | /**
32 | * Return the node that has caused this exception
33 | *
34 | * @return DOMNode
35 | */
36 | public function getNode()
37 | {
38 | return $this->node;
39 | }
40 |
41 | /**
42 | * Highlight the source of the template that has caused this exception, with the node highlighted
43 | *
44 | * @param string $prepend HTML to prepend
45 | * @param string $append HTML to append
46 | * @return string Template's source, as HTML
47 | */
48 | public function highlightNode($prepend = '', $append = '')
49 | {
50 | return TemplateHelper::highlightNode($this->node, $prepend, $append);
51 | }
52 |
53 | /**
54 | * Change the node associated with this exception
55 | *
56 | * @param DOMNode $node
57 | * @return void
58 | */
59 | public function setNode(DOMNode $node)
60 | {
61 | $this->node = $node;
62 | }
63 | }
--------------------------------------------------------------------------------
/src/Configurator/FilterableConfigValue.php:
--------------------------------------------------------------------------------
1 | '/',
25 | 'caseInsensitive' => false,
26 | 'specialChars' => [],
27 | 'unicode' => true
28 | ];
29 |
30 | // Normalize ASCII if the regexp is meant to be case-insensitive
31 | if ($options['caseInsensitive'])
32 | {
33 | foreach ($words as &$word)
34 | {
35 | $word = strtr($word, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
36 | }
37 | unset($word);
38 | }
39 |
40 | $builder = new Builder([
41 | 'delimiter' => $options['delimiter'],
42 | 'meta' => $options['specialChars'],
43 | 'input' => $options['unicode'] ? 'Utf8' : 'Bytes',
44 | 'output' => $options['unicode'] ? 'Utf8' : 'Bytes'
45 | ]);
46 |
47 | return $builder->build($words);
48 | }
49 | }
--------------------------------------------------------------------------------
/src/Configurator/Helpers/TemplateParser.php:
--------------------------------------------------------------------------------
1 | parse($template);
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilter.php:
--------------------------------------------------------------------------------
1 | resetParameters();
27 | $this->addParameterByName('attrValue');
28 | }
29 |
30 | /**
31 | * Return whether this filter makes a value safe to be used in JavaScript
32 | *
33 | * @return bool
34 | */
35 | public function isSafeInJS()
36 | {
37 | // List of callbacks that make a value safe to be used in a script, hardcoded for
38 | // convenience. Technically, there are numerous built-in PHP functions that would make an
39 | // arbitrary value safe in JS, but only a handful have the potential to be used as an
40 | // attribute filter
41 | $safeCallbacks = [
42 | 'urlencode',
43 | 'strtotime',
44 | 'rawurlencode'
45 | ];
46 |
47 | if (in_array($this->callback, $safeCallbacks, true))
48 | {
49 | return true;
50 | }
51 |
52 | return $this->isSafe('InJS');
53 | }
54 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/AbstractMapFilter.php:
--------------------------------------------------------------------------------
1 | vars['map']))
22 | {
23 | $name = preg_replace('(.*\\\\|Filter$)', '', get_class($this));
24 |
25 | throw new RuntimeException($name . " filter is missing a 'map' value");
26 | }
27 |
28 | return parent::asConfig();
29 | }
30 |
31 | /**
32 | * Assess the safeness of this attribute filter based on given list of strings
33 | *
34 | * @param string[] $strings
35 | * @return void
36 | */
37 | protected function assessSafeness(array $strings): void
38 | {
39 | $str = implode('', $strings);
40 | foreach (['AsURL', 'InCSS', 'InJS'] as $context)
41 | {
42 | $callback = ContextSafeness::class . '::getDisallowedCharacters' . $context;
43 | foreach ($callback() as $char)
44 | {
45 | if (strpos($str, $char) !== false)
46 | {
47 | continue 2;
48 | }
49 | }
50 |
51 | $methodName = 'markAsSafe' . $context;
52 | $this->$methodName();
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/AlnumFilter.php:
--------------------------------------------------------------------------------
1 | markAsSafeAsURL();
19 | $this->markAsSafeInCSS();
20 | }
21 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/ChoiceFilter.php:
--------------------------------------------------------------------------------
1 | setValues($values, $caseSensitive);
28 | }
29 | }
30 |
31 | /**
32 | * Set the list of allowed values
33 | *
34 | * @param array $values List of allowed values
35 | * @param bool $caseSensitive Whether the choice is case-sensitive
36 | * @return void
37 | */
38 | public function setValues(array $values, $caseSensitive = false)
39 | {
40 | if (!is_bool($caseSensitive))
41 | {
42 | throw new InvalidArgumentException('Argument 2 passed to ' . __METHOD__ . ' must be a boolean');
43 | }
44 |
45 | // Create a regexp based on the list of allowed values
46 | $regexp = RegexpBuilder::fromList($values, ['delimiter' => '/']);
47 | $regexp = '/^' . $regexp . '$/D';
48 |
49 | // Add the case-insensitive flag if applicable
50 | if (!$caseSensitive)
51 | {
52 | $regexp .= 'i';
53 | }
54 |
55 | // Add the Unicode flag if the regexp isn't purely ASCII
56 | if (!preg_match('#^[[:ascii:]]*$#D', $regexp))
57 | {
58 | $regexp .= 'u';
59 | }
60 |
61 | // Set the regexp associated with this list of values
62 | $this->setRegexp($regexp);
63 | }
64 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/ColorFilter.php:
--------------------------------------------------------------------------------
1 | markAsSafeInCSS();
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/EmailFilter.php:
--------------------------------------------------------------------------------
1 | setJS('EmailFilter.filter');
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/FalseFilter.php:
--------------------------------------------------------------------------------
1 | setJS('FalseFilter.filter');
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/FloatFilter.php:
--------------------------------------------------------------------------------
1 | setJS('NumericFilter.filterFloat');
21 | $this->markAsSafeAsURL();
22 | $this->markAsSafeInCSS();
23 | $this->markAsSafeInJS();
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/FontfamilyFilter.php:
--------------------------------------------------------------------------------
1 | markAsSafeInCSS();
23 | }
24 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/IdentifierFilter.php:
--------------------------------------------------------------------------------
1 | markAsSafeAsURL();
19 | $this->markAsSafeInCSS();
20 | }
21 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/IntFilter.php:
--------------------------------------------------------------------------------
1 | setJS('NumericFilter.filterInt');
21 | $this->markAsSafeAsURL();
22 | $this->markAsSafeInCSS();
23 | $this->markAsSafeInJS();
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/IpFilter.php:
--------------------------------------------------------------------------------
1 | setJS('NetworkFilter.filterIp');
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/IpportFilter.php:
--------------------------------------------------------------------------------
1 | setJS('NetworkFilter.filterIpport');
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/Ipv4Filter.php:
--------------------------------------------------------------------------------
1 | setJS('NetworkFilter.filterIpv4');
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/Ipv6Filter.php:
--------------------------------------------------------------------------------
1 | setJS('NetworkFilter.filterIpv6');
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/NumberFilter.php:
--------------------------------------------------------------------------------
1 | markAsSafeAsURL();
19 | $this->markAsSafeInCSS();
20 | $this->markAsSafeInJS();
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/SimpletextFilter.php:
--------------------------------------------------------------------------------
1 | markAsSafeInCSS();
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/TimestampFilter.php:
--------------------------------------------------------------------------------
1 | setJS('TimestampFilter.filter');
21 | $this->markAsSafeAsURL();
22 | $this->markAsSafeInCSS();
23 | $this->markAsSafeInJS();
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/UintFilter.php:
--------------------------------------------------------------------------------
1 | setJS('NumericFilter.filterUint');
21 | }
22 |
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function isSafeInCSS()
27 | {
28 | return true;
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function isSafeInJS()
35 | {
36 | return true;
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function isSafeAsURL()
43 | {
44 | return true;
45 | }
46 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributeFilters/UrlFilter.php:
--------------------------------------------------------------------------------
1 | resetParameters();
22 | $this->addParameterByName('attrValue');
23 | $this->addParameterByName('urlConfig');
24 | $this->addParameterByName('logger');
25 | $this->setJS('UrlFilter.filter');
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function isSafeInCSS()
32 | {
33 | return true;
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function isSafeInJS()
40 | {
41 | return true;
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | public function isSafeAsURL()
48 | {
49 | return true;
50 | }
51 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/AttributePreprocessor.php:
--------------------------------------------------------------------------------
1 | regexp]
19 | */
20 | public function getAttributes()
21 | {
22 | return $this->getNamedCaptures();
23 | }
24 |
25 | /**
26 | * Return the regexp this preprocessor is based on
27 | *
28 | * @return string
29 | */
30 | public function getRegexp()
31 | {
32 | return $this->regexp;
33 | }
34 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/Filter.php:
--------------------------------------------------------------------------------
1 | resetParameters();
23 | $this->addParameterByName('tag');
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/TemplateDocument.php:
--------------------------------------------------------------------------------
1 | template = $template;
30 | }
31 |
32 | /**
33 | * Update the original template with this document's content
34 | *
35 | * @return void
36 | */
37 | public function saveChanges()
38 | {
39 | $this->template->setContent(TemplateLoader::save($this));
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Configurator/Items/UnsafeTemplate.php:
--------------------------------------------------------------------------------
1 | code = $code;
30 | }
31 |
32 | /**
33 | * Return this source code
34 | *
35 | * @return string
36 | */
37 | public function __toString()
38 | {
39 | return (string) $this->code;
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | public function filterConfig($target)
46 | {
47 | return ($target === 'JS') ? $this : null;
48 | }
49 | }
--------------------------------------------------------------------------------
/src/Configurator/JavaScript/Dictionary.php:
--------------------------------------------------------------------------------
1 | getArrayCopy();
25 | if ($target === 'JS')
26 | {
27 | $value = new Dictionary(ConfigHelper::filterConfig($value, $target));
28 | }
29 |
30 | return $value;
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Configurator/JavaScript/Minifiers/MatthiasMullieMinify.php:
--------------------------------------------------------------------------------
1 | minify();
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Configurator/JavaScript/Minifiers/Noop.php:
--------------------------------------------------------------------------------
1 | httpClient = Http::getClient();
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Configurator/RecursiveParser/AbstractRecursiveMatcher.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
26 | }
27 |
28 | /**
29 | * Parse given string and return its value
30 | *
31 | * @param string $str
32 | * @param string $restrict Pipe-separated list of allowed matches (ignored if empty)
33 | * @return mixed
34 | */
35 | protected function recurse(string $str, string $restrict = '')
36 | {
37 | return $this->parser->parse($str, $restrict)['value'];
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Configurator/RecursiveParser/CachingRecursiveParser.php:
--------------------------------------------------------------------------------
1 | cache[$restrict][$str]))
25 | {
26 | $this->cache[$restrict][$str] = parent::parse($str, $restrict);
27 | }
28 |
29 | return $this->cache[$restrict][$str];
30 | }
31 |
32 | /**
33 | * {@inheritdoc}
34 | */
35 | public function setMatchers(array $matchers): void
36 | {
37 | $this->cache = [];
38 | parent::setMatchers($matchers);
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Configurator/RecursiveParser/MatcherInterface.php:
--------------------------------------------------------------------------------
1 | '((?&Boolean)) and ((?&BooleanExpression)|(?&Boolean))',
19 | 'Boolean:BooleanSubExpr' => '\\( ((?&BooleanExpression)|(?&Boolean)) \\)',
20 | 'BooleanExpression:Or' => '((?&Boolean)) or ((?&BooleanExpression)|(?&Boolean))'
21 | ];
22 | }
23 |
24 | /**
25 | * Convert a "and" operation
26 | *
27 | * @param string $expr1
28 | * @param string $expr2
29 | * @return string
30 | */
31 | public function parseAnd($expr1, $expr2)
32 | {
33 | return $this->recurse($expr1) . '&&' . $this->recurse($expr2);
34 | }
35 |
36 | /**
37 | * Convert a boolean subexpression
38 | *
39 | * @param string $expr
40 | * @return string
41 | */
42 | public function parseBooleanSubExpr($expr)
43 | {
44 | return '(' . $this->recurse($expr) . ')';
45 | }
46 |
47 | /**
48 | * Convert a "or" operation
49 | *
50 | * @param string $expr1
51 | * @param string $expr2
52 | * @return string
53 | */
54 | public function parseOr($expr1, $expr2)
55 | {
56 | return $this->recurse($expr1) . '||' . $this->recurse($expr2);
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Configurator/RendererGenerators/Unformatted.php:
--------------------------------------------------------------------------------
1 | isVoid()) ? ['autoClose' => true] : [];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/AutoReopenFormattingElements.php:
--------------------------------------------------------------------------------
1 | isFormattingElement()) ? ['autoReopen' => true] : [];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/BlockElementsCloseFormattingElements.php:
--------------------------------------------------------------------------------
1 | isBlock() && $trg->isFormattingElement()) ? ['closeParent'] : [];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/BlockElementsFosterFormattingElements.php:
--------------------------------------------------------------------------------
1 | isBlock() && $src->isPassthrough() && $trg->isFormattingElement()) ? ['fosterParent'] : [];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/DisableAutoLineBreaksIfNewLinesArePreserved.php:
--------------------------------------------------------------------------------
1 | preservesNewLines()) ? ['disableAutoLineBreaks' => true] : [];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/EnforceOptionalEndTags.php:
--------------------------------------------------------------------------------
1 | closesParent($trg)) ? ['closeParent'] : [];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/IgnoreTagsInCode.php:
--------------------------------------------------------------------------------
1 | evaluate('count(//code//xsl:apply-templates)')) ? ['ignoreTags' => true] : [];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/IgnoreTextIfDisallowed.php:
--------------------------------------------------------------------------------
1 | allowsText()) ? [] : ['ignoreText' => true];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/IgnoreWhitespaceAroundBlockElements.php:
--------------------------------------------------------------------------------
1 | isBlock()) ? ['ignoreSurroundingWhitespace' => true] : [];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/Interfaces/BooleanRulesGenerator.php:
--------------------------------------------------------------------------------
1 | bool]
19 | */
20 | public function generateBooleanRules(TemplateInspector $src);
21 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/Interfaces/TargetedRulesGenerator.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | public function __construct()
26 | {
27 | $this->p = new TemplateInspector('
');
28 | }
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | public function generateBooleanRules(TemplateInspector $src)
34 | {
35 | $rules = [];
36 |
37 | if ($src->allowsChild($this->p) && $src->isBlock() && !$this->p->closesParent($src))
38 | {
39 | $rules['createParagraphs'] = true;
40 | }
41 |
42 | if ($src->closesParent($this->p))
43 | {
44 | $rules['breakParagraph'] = true;
45 | }
46 |
47 | return $rules;
48 | }
49 | }
--------------------------------------------------------------------------------
/src/Configurator/RulesGenerators/TrimFirstLineInCodeBlocks.php:
--------------------------------------------------------------------------------
1 | evaluate('count(//pre//code//xsl:apply-templates)')) ? ['trimFirstLine' => true] : [];
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateCheck.php:
--------------------------------------------------------------------------------
1 | node
27 | * @param Tag $tag Tag this template belongs to
28 | * @return void
29 | */
30 | abstract public function check(DOMElement $template, Tag $tag);
31 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowAttributeSets.php:
--------------------------------------------------------------------------------
1 |
20 | *
21 | * Templates are checked outside of their stylesheet, which means we don't have access to the
22 | * declarations and we can't easily test them. Attribute sets are fairly
23 | * uncommon and there's little incentive to use them in small stylesheets
24 | *
25 | * @param DOMElement $template node
26 | * @param Tag $tag Tag this template belongs to
27 | * @return void
28 | */
29 | public function check(DOMElement $template, Tag $tag)
30 | {
31 | $xpath = new DOMXPath($template->ownerDocument);
32 | $nodes = $xpath->query('//@use-attribute-sets');
33 |
34 | if ($nodes->length)
35 | {
36 | throw new UnsafeTemplateException('Cannot assess the safety of attribute sets', $nodes->item(0));
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowCopy.php:
--------------------------------------------------------------------------------
1 | elements
19 | *
20 | * @param DOMElement $template node
21 | * @param Tag $tag Tag this template belongs to
22 | * @return void
23 | */
24 | public function check(DOMElement $template, Tag $tag)
25 | {
26 | $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'copy');
27 | $node = $nodes->item(0);
28 |
29 | if ($node)
30 | {
31 | throw new UnsafeTemplateException("Cannot assess the safety of an '" . $node->nodeName . "' element", $node);
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowDisableOutputEscaping.php:
--------------------------------------------------------------------------------
1 | node
22 | * @param Tag $tag Tag this template belongs to
23 | * @return void
24 | */
25 | public function check(DOMElement $template, Tag $tag)
26 | {
27 | $xpath = new DOMXPath($template->ownerDocument);
28 | $node = $xpath->query('//@disable-output-escaping')->item(0);
29 |
30 | if ($node)
31 | {
32 | throw new UnsafeTemplateException("The template contains a 'disable-output-escaping' attribute", $node);
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowDynamicAttributeNames.php:
--------------------------------------------------------------------------------
1 | node using a dynamic name
19 | *
20 | * @param DOMElement $template node
21 | * @param Tag $tag Tag this template belongs to
22 | * @return void
23 | */
24 | public function check(DOMElement $template, Tag $tag)
25 | {
26 | $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'attribute');
27 | foreach ($nodes as $node)
28 | {
29 | if (strpos($node->getAttribute('name'), '{') !== false)
30 | {
31 | throw new UnsafeTemplateException('Dynamic names are disallowed', $node);
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowDynamicElementNames.php:
--------------------------------------------------------------------------------
1 | node using a dynamic name
19 | *
20 | * @param DOMElement $template node
21 | * @param Tag $tag Tag this template belongs to
22 | * @return void
23 | */
24 | public function check(DOMElement $template, Tag $tag)
25 | {
26 | $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'element');
27 | foreach ($nodes as $node)
28 | {
29 | if (strpos($node->getAttribute('name'), '{') !== false)
30 | {
31 | throw new UnsafeTemplateException('Dynamic names are disallowed', $node);
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowElement.php:
--------------------------------------------------------------------------------
1 | elName = strtolower($elName);
32 | }
33 |
34 | /**
35 | * Test for the presence of an element of given name
36 | *
37 | * @param DOMElement $template node
38 | * @param Tag $tag Tag this template belongs to
39 | * @return void
40 | */
41 | public function check(DOMElement $template, Tag $tag)
42 | {
43 | $xpath = new DOMXPath($template->ownerDocument);
44 | $query
45 | = '//*[translate(local-name(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "' . $this->elName . '"]'
46 | . '|'
47 | . '//xsl:element[translate(@name,"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "' . $this->elName . '"]';
48 |
49 | $node = $xpath->query($query)->item(0);
50 | if ($node)
51 | {
52 | throw new UnsafeTemplateException("Element '" . $this->elName . "' is disallowed", $node);
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowElementNS.php:
--------------------------------------------------------------------------------
1 | namespaceURI = $namespaceURI;
36 | $this->elName = $elName;
37 | }
38 |
39 | /**
40 | * Test for the presence of an element of given name in given namespace
41 | *
42 | * @param DOMElement $template node
43 | * @param Tag $tag Tag this template belongs to
44 | * @return void
45 | */
46 | public function check(DOMElement $template, Tag $tag)
47 | {
48 | $node = $template->getElementsByTagNameNS($this->namespaceURI, $this->elName)->item(0);
49 |
50 | if ($node)
51 | {
52 | throw new UnsafeTemplateException("Element '" . $node->nodeName . "' is disallowed", $node);
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowFlashFullScreen.php:
--------------------------------------------------------------------------------
1 | 1,
28 | 'false' => 0
29 | ];
30 |
31 | /**
32 | * Constructor
33 | *
34 | * @param bool $onlyIfDynamic Whether this restriction applies only to elements using any kind
35 | * of dynamic markup: XSL elements or attribute value templates
36 | */
37 | public function __construct($onlyIfDynamic = false)
38 | {
39 | parent::__construct('false', $onlyIfDynamic);
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowNodeByXPath.php:
--------------------------------------------------------------------------------
1 | query = $query;
31 | }
32 |
33 | /**
34 | * Test for the presence of an element of given name
35 | *
36 | * @param DOMElement $template node
37 | * @param Tag $tag Tag this template belongs to
38 | * @return void
39 | */
40 | public function check(DOMElement $template, Tag $tag)
41 | {
42 | $xpath = new DOMXPath($template->ownerDocument);
43 |
44 | foreach ($xpath->query($this->query) as $node)
45 | {
46 | throw new UnsafeTemplateException("Node '" . $node->nodeName . "' is disallowed because it matches '" . $this->query . "'", $node);
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowObjectParamsWithGeneratedName.php:
--------------------------------------------------------------------------------
1 | elements with a generated "name" attribute
20 | *
21 | * This check will reject elements whose "name" attribute is generated by an
22 | * element. This is a setup that has no practical use and should be eliminated
23 | * because it makes it much harder to check the param's name, and therefore infer the type of
24 | * content it expects
25 | *
26 | * @param DOMElement $template node
27 | * @param Tag $tag Tag this template belongs to
28 | * @return void
29 | */
30 | public function check(DOMElement $template, Tag $tag)
31 | {
32 | $xpath = new DOMXPath($template->ownerDocument);
33 | $query = '//object//param[contains(@name, "{") or .//xsl:attribute[translate(@name, "NAME", "name") = "name"]]';
34 | $nodes = $xpath->query($query);
35 |
36 | foreach ($nodes as $node)
37 | {
38 | throw new UnsafeTemplateException("A 'param' element with a suspect name has been found", $node);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowUncompilableXSL.php:
--------------------------------------------------------------------------------
1 | getAttribute('select');
22 | if (!preg_match('#^@[-\\w]+(?:\\s*\\|\\s*@[-\\w]+)*$#', $expr) && $expr !== '@*')
23 | {
24 | throw new RuntimeException("Unsupported xsl:copy-of expression '" . $expr . "'");
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowUnsafeCopyOf.php:
--------------------------------------------------------------------------------
1 | elements
19 | *
20 | * Any select expression that is not a set of named attributes is considered unsafe
21 | *
22 | * @param DOMElement $template node
23 | * @param Tag $tag Tag this template belongs to
24 | * @return void
25 | */
26 | public function check(DOMElement $template, Tag $tag)
27 | {
28 | $nodes = $template->getElementsByTagNameNS(self::XMLNS_XSL, 'copy-of');
29 | foreach ($nodes as $node)
30 | {
31 | $expr = $node->getAttribute('select');
32 |
33 | if (!preg_match('#^@[-\\w]*(?:\\s*\\|\\s*@[-\\w]*)*$#D', $expr))
34 | {
35 | throw new UnsafeTemplateException("Cannot assess the safety of '" . $node->nodeName . "' select expression '" . $expr . "'", $node);
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowUnsafeDynamicCSS.php:
--------------------------------------------------------------------------------
1 | ownerDocument);
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | protected function isExpressionSafe($expr)
29 | {
30 | return XPathHelper::isExpressionNumeric($expr);
31 | }
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | protected function isSafe(Attribute $attribute)
37 | {
38 | return $attribute->isSafeInCSS();
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/DisallowUnsafeDynamicJS.php:
--------------------------------------------------------------------------------
1 | ownerDocument);
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | protected function isExpressionSafe($expr)
29 | {
30 | return XPathHelper::isExpressionNumeric($expr);
31 | }
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | protected function isSafe(Attribute $attribute)
37 | {
38 | return $attribute->isSafeInJS();
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/RestrictFlashNetworking.php:
--------------------------------------------------------------------------------
1 | 3,
28 | 'internal' => 2,
29 | 'none' => 1
30 | ];
31 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateChecks/RestrictFlashScriptAccess.php:
--------------------------------------------------------------------------------
1 | 3,
28 | 'samedomain' => 2,
29 | 'never' => 1
30 | ];
31 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/AddAttributeValueToElements.php:
--------------------------------------------------------------------------------
1 | attrName = $attrName;
35 | $this->queries = [$query];
36 | $this->value = $value;
37 | }
38 |
39 | /**
40 | * Explode a string of space-separated values into an array
41 | *
42 | * @param string $attrValue Attribute's value
43 | * @return string[]
44 | */
45 | protected function getValues(string $attrValue): array
46 | {
47 | return preg_match_all('(\\S++)', $attrValue, $m) ? $m[0] : [];
48 | }
49 |
50 | /**
51 | * {@inheritdoc}
52 | */
53 | protected function normalizeElement(Element $element): void
54 | {
55 | $currentValues = $this->getValues($element->getAttribute($this->attrName));
56 | if (!in_array($this->value, $currentValues, true))
57 | {
58 | $currentValues[] = $this->value;
59 |
60 | $element->setAttribute($this->attrName, implode(' ', $currentValues));
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/ConvertCurlyExpressionsInText.php:
--------------------------------------------------------------------------------
1 | {$FOO}{@bar}
17 | * with
18 | *
19 | */
20 | class ConvertCurlyExpressionsInText extends AbstractNormalization
21 | {
22 | /**
23 | * {@inheritdoc}
24 | */
25 | protected array $queries = ['//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/text()[contains(., "{@") or contains(., "{$")]'];
26 |
27 | /**
28 | * Insert a text node before given node
29 | */
30 | protected function insertTextBefore(string $text, Text $node): void
31 | {
32 | if ($text > '')
33 | {
34 | $node->before($this->createPolymorphicText($text));
35 | }
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function normalizeText(Text $node): void
42 | {
43 | preg_match_all(
44 | '#\\{([$@][-\\w]+)\\}#',
45 | $node->textContent,
46 | $matches,
47 | PREG_SET_ORDER | PREG_OFFSET_CAPTURE
48 | );
49 |
50 | $lastPos = 0;
51 | foreach ($matches as $m)
52 | {
53 | $pos = $m[0][1];
54 |
55 | // Catch up to current position
56 | if ($pos > $lastPos)
57 | {
58 | $text = substr($node->textContent, $lastPos, $pos - $lastPos);
59 | $this->insertTextBefore($text, $node);
60 | }
61 | $lastPos = $pos + strlen($m[0][0]);
62 |
63 | // Add the xsl:value-of element
64 | $node->beforeXslValueOf($m[1][0]);
65 | }
66 |
67 | // Append the rest of the text
68 | $text = substr($node->textContent, $lastPos);
69 | $this->insertTextBefore($text, $node);
70 |
71 | // Now remove the old text node
72 | $node->remove();
73 | }
74 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/Custom.php:
--------------------------------------------------------------------------------
1 | callback = $callback;
27 | }
28 |
29 | /**
30 | * Call the user-supplied callback
31 | *
32 | * @param Element $template node
33 | * @return void
34 | */
35 | public function normalize(Element $template): void
36 | {
37 | call_user_func($this->callback, $template);
38 | $this->reset();
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/DeoptimizeIf.php:
--------------------------------------------------------------------------------
1 | replaceWithXslChoose();
28 | $when = $choose->appendXslWhen($element->getAttribute('test'));
29 | $when->append(...$element->childNodes);
30 | }
31 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/EnforceHTMLOmittedEndTags.php:
--------------------------------------------------------------------------------
1 | ..
18 | * with
19 | * .
.
20 | */
21 | class EnforceHTMLOmittedEndTags extends AbstractNormalization
22 | {
23 | /**
24 | * {@inheritdoc}
25 | */
26 | protected array $queries = ['//*[namespace-uri() = ""]/*[namespace-uri() = ""]'];
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function normalizeElement(Element $element): void
32 | {
33 | $parentNode = $element->parentNode;
34 | if (ElementInspector::isVoid($parentNode) || ElementInspector::closesParent($element, $parentNode))
35 | {
36 | $this->reparentElement($element);
37 | }
38 | }
39 |
40 | /**
41 | * Move given element and its following siblings after its parent element
42 | *
43 | * @param Element $element First element to move
44 | * @return void
45 | */
46 | protected function reparentElement(Element $element)
47 | {
48 | $parentNode = $element->parentNode;
49 | do
50 | {
51 | $lastChild = $parentNode->lastChild;
52 | $parentNode->parentNode->insertBefore($lastChild, $parentNode->nextSibling);
53 | }
54 | while (!$lastChild->isSameNode($element));
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/FixUnescapedCurlyBracesInHtmlAttributes.php:
--------------------------------------------------------------------------------
1 |
17 | *
18 | * with
19 | *
20 | *
21 | */
22 | class FixUnescapedCurlyBracesInHtmlAttributes extends AbstractNormalization
23 | {
24 | /**
25 | * {@inheritdoc}
26 | */
27 | protected array $queries = ['//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{")]'];
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function normalizeAttribute(Attr $attribute): void
33 | {
34 | $match = [
35 | '(\\b(?:do|else|(?:if|while)\\s*\\(.*?\\))\\s*\\{(?![{@]))',
36 | '(\\bfunction\\s*\\w*\\s*\\([^\\)]*\\)\\s*\\{(?!\\{))',
37 | '(=(?:>|>)\\s*\\{(?!\\{))',
38 | '((?value);
49 | $attribute->value = htmlspecialchars($attrValue, ENT_NOQUOTES, 'UTF-8');
50 | }
51 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/InlineAttributes.php:
--------------------------------------------------------------------------------
1 | ...
18 | * with
19 | * ...
20 | */
21 | class InlineAttributes extends AbstractNormalization
22 | {
23 | /**
24 | * {@inheritdoc}
25 | */
26 | protected array $queries = ['//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/xsl:attribute'];
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function normalizeElement(Element $element): void
32 | {
33 | $value = '';
34 | foreach ($element->childNodes as $node)
35 | {
36 | if ($node instanceof Text || $this->isXsl($node, 'text'))
37 | {
38 | $value .= preg_replace('([{}])', '$0$0', $node->textContent);
39 | }
40 | elseif ($this->isXsl($node, 'value-of'))
41 | {
42 | $value .= '{' . $node->getAttribute('select') . '}';
43 | }
44 | else
45 | {
46 | // Can't inline this attribute
47 | return;
48 | }
49 | }
50 | $element->parentNode->setAttribute($element->getAttribute('name'), $value);
51 | $element->remove();
52 | }
53 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/InlineCDATA.php:
--------------------------------------------------------------------------------
1 | replaceWith($this->createPolymorphicText($cdata->textContent));
25 | }
26 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/InlineElements.php:
--------------------------------------------------------------------------------
1 |
18 | * with
19 | *
20 | */
21 | class InlineElements extends AbstractNormalization
22 | {
23 | /**
24 | * {@inheritdoc}
25 | */
26 | protected array $queries = ['//xsl:element'];
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function normalizeElement(Element $element): void
32 | {
33 | $elName = $element->getAttribute('name');
34 | $dom = $element->ownerDocument;
35 |
36 | try
37 | {
38 | // Create the new static element
39 | $newElement = ($element->hasAttribute('namespace'))
40 | ? $dom->createElementNS($element->getAttribute('namespace'), $elName)
41 | : $dom->createElement($elName);
42 | }
43 | catch (DOMException $e)
44 | {
45 | // Ignore this element if an exception got thrown
46 | return;
47 | }
48 |
49 | // Replace the old with it. We do it now so that libxml doesn't have to
50 | // redeclare the XSL namespace
51 | $element->replaceWith($newElement);
52 |
53 | // Move all the nodes from the old element to the new one
54 | $newElement->append(...$element->childNodes);
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/InlineTextElements.php:
--------------------------------------------------------------------------------
1 | nextSibling && $element->nextSibling->nodeType === XML_TEXT_NODE);
28 | }
29 |
30 | /**
31 | * Test whether an element is preceded by a text node
32 | *
33 | * @param Element $element
34 | * @return bool
35 | */
36 | protected function isPrecededByText(Element $element)
37 | {
38 | return ($element->previousSibling && $element->previousSibling->nodeType === XML_TEXT_NODE);
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function normalizeElement(Element $element): void
45 | {
46 | // If this node's content is whitespace, ensure it's preceded or followed by a text node
47 | if (trim($element->textContent) === '')
48 | {
49 | if (!$this->isFollowedByText($element) && !$this->isPrecededByText($element))
50 | {
51 | // This would become inter-element whitespace, therefore we can't inline
52 | return;
53 | }
54 | }
55 | $element->replaceWith($element->textContent);
56 | }
57 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/InlineXPathLiterals.php:
--------------------------------------------------------------------------------
1 | getTextContent($token[1]);
53 | if ($textContent !== false)
54 | {
55 | // Turn this token into a literal
56 | $token = ['literal', $textContent];
57 | }
58 | }
59 |
60 | return $token;
61 | }
62 | );
63 | }
64 |
65 | /**
66 | * {@inheritdoc}
67 | */
68 | protected function normalizeElement(Element $element): void
69 | {
70 | $textContent = $this->getTextContent($element->getAttribute('select'));
71 | if ($textContent !== false)
72 | {
73 | $element->replaceWith($this->createPolymorphicText($textContent));
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/MergeConsecutiveCopyOf.php:
--------------------------------------------------------------------------------
1 | nextSiblingIsCopyOf($element))
25 | {
26 | $element->setAttribute('select', $element->getAttribute('select') . '|' . $element->nextSibling->getAttribute('select'));
27 | $element->nextSibling->remove();
28 | }
29 | }
30 |
31 | /**
32 | * Test whether the next sibling to given element is an xsl:copy-of element
33 | *
34 | * @param Element $element Context node
35 | * @return bool
36 | */
37 | protected function nextSiblingIsCopyOf(Element $element)
38 | {
39 | return ($element->nextSibling && $this->isXsl($element->nextSibling, 'copy-of'));
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/MinifyInlineCSS.php:
--------------------------------------------------------------------------------
1 | nodeValue;
26 |
27 | // Only minify if the value does not contain any XPath expression that's not an attribute
28 | if (!preg_match('(\\{(?!@\\w+\\}))', $css))
29 | {
30 | $attribute->nodeValue = $this->minify($css);
31 | }
32 | }
33 |
34 | /**
35 | * Minify a CSS string
36 | *
37 | * @param string $css Original CSS
38 | * @return string Minified CSS
39 | */
40 | protected function minify($css)
41 | {
42 | $css = trim($css, " \n\t;");
43 | $css = preg_replace('(\\s*([,:;])\\s*)', '$1', $css);
44 | $css = preg_replace_callback(
45 | '((?<=[\\s:])#[0-9a-f]{3,6})i',
46 | function ($m)
47 | {
48 | return strtolower($m[0]);
49 | },
50 | $css
51 | );
52 | $css = preg_replace('((?<=[\\s:])#([0-9a-f])\\1([0-9a-f])\\2([0-9a-f])\\3)', '#$1$2$3', $css);
53 | $css = preg_replace('((?<=[\\s:])#f00\\b)', 'red', $css);
54 | $css = preg_replace('((?<=[\\s:])0px\\b)', '0', $css);
55 |
56 | return $css;
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/MinifyXPathExpressions.php:
--------------------------------------------------------------------------------
1 | parentNode;
27 | if (!$this->isXsl($element))
28 | {
29 | // Replace XPath expressions in non-XSL elements
30 | $this->replaceAVT($attribute);
31 | }
32 | elseif (in_array($attribute->nodeName, ['match', 'select', 'test'], true))
33 | {
34 | // Replace the content of match, select and test attributes of an XSL element
35 | $expr = XPathHelper::minify($attribute->nodeValue);
36 | $element->setAttribute($attribute->nodeName, $expr);
37 | }
38 | }
39 |
40 | /**
41 | * Minify XPath expressions in given attribute
42 | */
43 | protected function replaceAVT(Attr $attribute)
44 | {
45 | AVTHelper::replace(
46 | $attribute,
47 | function ($token)
48 | {
49 | if ($token[0] === 'expression')
50 | {
51 | $token[1] = XPathHelper::minify($token[1]);
52 | }
53 |
54 | return $token;
55 | }
56 | );
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/NormalizeAttributeNames.php:
--------------------------------------------------------------------------------
1 | parentNode->setAttribute($this->lowercase($attribute->localName), $attribute->value);
29 | $attribute->parentNode->removeAttributeNode($attribute);
30 | }
31 |
32 | /**
33 | * {@inheritdoc}
34 | */
35 | protected function normalizeElement(Element $element): void
36 | {
37 | $element->setAttribute('name', $this->lowercase($element->getAttribute('name')));
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/OptimizeConditionalAttributes.php:
--------------------------------------------------------------------------------
1 | , e.g.
16 | *
17 | *
18 | *
19 | *
20 | *
21 | * into
22 | *
23 | */
24 | class OptimizeConditionalAttributes extends AbstractNormalization
25 | {
26 | /**
27 | * {@inheritdoc}
28 | */
29 | protected array $queries = ['//xsl:if[starts-with(@test, "@")][count(descendant::node()) = 2][xsl:attribute[@name = substring(../@test, 2)][xsl:value-of[@select = ../../@test]]]'];
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | protected function normalizeElement(Element $element): void
35 | {
36 | $element->replaceWithXslCopyOf($element->getAttribute('test'));
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/OptimizeConditionalValueOf.php:
--------------------------------------------------------------------------------
1 | tests around
14 | *
15 | * NOTE: should be performed before attributes are inlined for maximum effect
16 | */
17 | class OptimizeConditionalValueOf extends AbstractNormalization
18 | {
19 | /**
20 | * {@inheritdoc}
21 | */
22 | protected array $queries = ['//xsl:if[count(descendant::node()) = 1]/xsl:value-of'];
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | protected function normalizeElement(Element $element): void
28 | {
29 | $if = $element->parentNode;
30 | $test = $if->getAttribute('test');
31 | $select = $element->getAttribute('select');
32 |
33 | // Ensure that the expressions match, and that they select one single attribute
34 | if ($select !== $test || !preg_match('#^@[-\\w]+$#D', $select))
35 | {
36 | return;
37 | }
38 |
39 | // Replace the xsl:if element with the xsl:value-of element
40 | $if->replaceWith($element);
41 | }
42 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/OptimizeNestedConditionals.php:
--------------------------------------------------------------------------------
1 | parentNode;
34 | $outerChoose = $otherwise->parentNode;
35 | $outerChoose->append(...$element->childNodes);
36 |
37 | $otherwise->remove();
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/PreserveSingleSpaces.php:
--------------------------------------------------------------------------------
1 | replaceWithXslText(' ');
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/RemoveComments.php:
--------------------------------------------------------------------------------
1 | remove();
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/RemoveInterElementWhitespace.php:
--------------------------------------------------------------------------------
1 | remove();
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/RemoveLivePreviewAttributes.php:
--------------------------------------------------------------------------------
1 | parentNode->removeAttributeNode($attribute);
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | protected function normalizeElement(Element $element): void
38 | {
39 | $element->remove();
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/RenameLivePreviewEvent.php:
--------------------------------------------------------------------------------
1 | value = 'data-s9e-livepreview-onrender';
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | protected function normalizeElement(Element $element): void
38 | {
39 | $value = $element->getAttribute('data-s9e-livepreview-postprocess');
40 | $element->setAttribute('data-s9e-livepreview-onrender', $value);
41 | $element->removeAttribute('data-s9e-livepreview-postprocess');
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/SetAttributeOnElements.php:
--------------------------------------------------------------------------------
1 | getAttribute('rel')))
35 | {
36 | parent::normalizeElement($element);
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/SortAttributesByName.php:
--------------------------------------------------------------------------------
1 | attributes as $name => $attribute)
31 | {
32 | $attributes[$name] = $element->removeAttributeNode($attribute);
33 | }
34 |
35 | ksort($attributes);
36 | foreach ($attributes as $attribute)
37 | {
38 | $element->setAttributeNode($attribute);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Configurator/TemplateNormalizations/TransposeComments.php:
--------------------------------------------------------------------------------
1 | replaceWithXslComment($comment->textContent);
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Configurator/Validators/AttributeName.php:
--------------------------------------------------------------------------------
1 | } map
6 | * @return {*}
7 | */
8 | filter: function(attrValue, map)
9 | {
10 | let i = -1, cnt = map.length;
11 | while (++i < cnt)
12 | {
13 | if (map[i][0].test(attrValue))
14 | {
15 | return map[i][1];
16 | }
17 | }
18 |
19 | return attrValue;
20 | }
21 | };
--------------------------------------------------------------------------------
/src/Parser/AttributeFilters/MapFilter.php:
--------------------------------------------------------------------------------
1 | , ]]
19 | * @return mixed Filtered value, or FALSE if invalid
20 | */
21 | public static function filter($attrValue, array $map)
22 | {
23 | foreach ($map as $pair)
24 | {
25 | if (preg_match($pair[0], $attrValue))
26 | {
27 | return $pair[1];
28 | }
29 | }
30 |
31 | return $attrValue;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Parser/AttributeFilters/NetworkFilter.js:
--------------------------------------------------------------------------------
1 | const NetworkFilter =
2 | {
3 | /**
4 | * @param {*} attrValue
5 | * @return {*}
6 | */
7 | filterIp: function(attrValue)
8 | {
9 | if (/^[\d.]+$/.test(attrValue))
10 | {
11 | return NetworkFilter.filterIpv4(attrValue);
12 | }
13 |
14 | if (/^[\da-f:]+$/i.test(attrValue))
15 | {
16 | return NetworkFilter.filterIpv6(attrValue);
17 | }
18 |
19 | return false;
20 | },
21 |
22 | /**
23 | * @param {*} attrValue
24 | * @return {*}
25 | */
26 | filterIpport: function(attrValue)
27 | {
28 | let m, ip;
29 |
30 | if (m = /^\[([\da-f:]+)(\]:[1-9]\d*)$/i.exec(attrValue))
31 | {
32 | ip = NetworkFilter.filterIpv6(m[1]);
33 |
34 | if (ip === false)
35 | {
36 | return false;
37 | }
38 |
39 | return '[' + ip + m[2];
40 | }
41 |
42 | if (m = /^([\d.]+)(:[1-9]\d*)$/.exec(attrValue))
43 | {
44 | ip = NetworkFilter.filterIpv4(m[1]);
45 |
46 | if (ip === false)
47 | {
48 | return false;
49 | }
50 |
51 | return ip + m[2];
52 | }
53 |
54 | return false;
55 | },
56 |
57 | /**
58 | * @param {*} attrValue
59 | * @return {*}
60 | */
61 | filterIpv4: function(attrValue)
62 | {
63 | if (!/^\d+\.\d+\.\d+\.\d+$/.test(attrValue))
64 | {
65 | return false;
66 | }
67 |
68 | let i = 4, p = attrValue.split('.');
69 | while (--i >= 0)
70 | {
71 | // NOTE: ext/filter doesn't support octal notation
72 | if (p[i][0] === '0' || p[i] > 255)
73 | {
74 | return false;
75 | }
76 | }
77 |
78 | return attrValue;
79 | },
80 |
81 | /**
82 | * @param {*} attrValue
83 | * @return {*}
84 | */
85 | filterIpv6: function(attrValue)
86 | {
87 | return /^([\da-f]{0,4}:){2,7}(?:[\da-f]{0,4}|\d+\.\d+\.\d+\.\d+)$/.test(attrValue) ? attrValue : false;
88 | }
89 | };
--------------------------------------------------------------------------------
/src/Parser/AttributeFilters/NetworkFilter.php:
--------------------------------------------------------------------------------
1 | max)
55 | {
56 | if (logger)
57 | {
58 | logger.warn(
59 | 'Value outside of range, adjusted down to max value',
60 | {
61 | 'attrValue' : attrValue,
62 | 'min' : min,
63 | 'max' : max
64 | }
65 | );
66 | }
67 |
68 | return max;
69 | }
70 |
71 | return attrValue;
72 | },
73 |
74 | /**
75 | * @param {*} attrValue
76 | * @return {*}
77 | */
78 | filterUint: function(attrValue)
79 | {
80 | return /^(?:0|[1-9]\d*)$/.test(attrValue) ? attrValue : false;
81 | }
82 | };
--------------------------------------------------------------------------------
/src/Parser/AttributeFilters/RegexpFilter.js:
--------------------------------------------------------------------------------
1 | const RegexpFilter =
2 | {
3 | /**
4 | * @param {*} attrValue
5 | * @param {!RegExp} regexp
6 | * @return {*}
7 | */
8 | filter: function(attrValue, regexp)
9 | {
10 | return regexp.test(attrValue) ? attrValue : false;
11 | }
12 | };
--------------------------------------------------------------------------------
/src/Parser/AttributeFilters/RegexpFilter.php:
--------------------------------------------------------------------------------
1 | ['regexp' => $regexp]
23 | ]);
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Parser/AttributeFilters/TimestampFilter.js:
--------------------------------------------------------------------------------
1 | const TimestampFilter =
2 | {
3 | /**
4 | * @param {*} attrValue
5 | * @return {*}
6 | */
7 | filter: function(attrValue)
8 | {
9 | let m = /^(?=\d)(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(attrValue);
10 | if (m)
11 | {
12 | return 3600 * (m[1] || 0) + 60 * (m[2] || 0) + (+m[3] || 0);
13 | }
14 |
15 | return NumericFilter.filterUint(attrValue);
16 | }
17 | };
--------------------------------------------------------------------------------
/src/Parser/AttributeFilters/TimestampFilter.php:
--------------------------------------------------------------------------------
1 |
12 | b.innerHTML = str.replace(/&"]/g,
28 | /**
29 | * @param {string} c
30 | * @return {string}
31 | */
32 | (c) =>
33 | {
34 | const t = {
35 | '<' : '<',
36 | '>' : '>',
37 | '&' : '&',
38 | '"' : '"'
39 | };
40 | return t[c];
41 | }
42 | );
43 | }
44 |
45 | /**
46 | * @param {string} str
47 | * @return {string}
48 | */
49 | function htmlspecialchars_noquotes(str)
50 | {
51 | return str.replace(
52 | /[<>&]/g,
53 | /**
54 | * @param {string} c
55 | * @return {string}
56 | */
57 | (c) =>
58 | {
59 | const t = {
60 | '<' : '<',
61 | '>' : '>',
62 | '&' : '&'
63 | };
64 | return t[c];
65 | }
66 | );
67 | }
68 |
69 | /**
70 | * @param {string} str
71 | * @return {string}
72 | */
73 | function rawurlencode(str)
74 | {
75 | return encodeURIComponent(str).replace(
76 | /[!'()*]/g,
77 | /**
78 | * @param {string} c
79 | * @return {string}
80 | */
81 | (c) =>
82 | {
83 | return '%' + c.charCodeAt(0).toString(16).toUpperCase();
84 | }
85 | );
86 | }
87 |
88 | /**
89 | * @return {boolean}
90 | */
91 | function returnFalse()
92 | {
93 | return false;
94 | }
95 |
96 | /**
97 | * @return {boolean}
98 | */
99 | function returnTrue()
100 | {
101 | return true;
102 | }
--------------------------------------------------------------------------------
/src/Plugins/AbstractStaticUrlReplacer/AbstractParser.php:
--------------------------------------------------------------------------------
1 | config['tagName'];
22 | $attrName = $this->config['attrName'];
23 | $prio = $this->tagPriority;
24 | foreach ($matches as $m)
25 | {
26 | $this->parser->addTagPair($tagName, $m[0][1], 0, $m[0][1] + strlen($m[0][0]), 0, $prio)
27 | ->setAttribute($attrName, $m[0][0]);
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Plugins/AbstractStaticUrlReplacer/Parser.js:
--------------------------------------------------------------------------------
1 | const tagName = config.tagName,
2 | attrName = config.attrName,
3 | prio = tagPriority || 0;
4 |
5 | matches.forEach((m) =>
6 | {
7 | addTagPair(tagName, m[0][1], 0, m[0][1] + m[0][0].length, 0, prio).setAttribute(attrName, m[0][0]);
8 | });
--------------------------------------------------------------------------------
/src/Plugins/Autoemail/Configurator.php:
--------------------------------------------------------------------------------
1 | configurator->tags[$this->tagName]))
42 | {
43 | return;
44 | }
45 |
46 | // Create a tag
47 | $tag = $this->configurator->tags->add($this->tagName);
48 |
49 | // Add an attribute using the default email filter
50 | $filter = $this->configurator->attributeFilters->get('#email');
51 | $tag->attributes->add($this->attrName)->filterChain->append($filter);
52 |
53 | // Set the default template
54 | $tag->template = '';
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Plugins/Autoemail/Parser.js:
--------------------------------------------------------------------------------
1 | let tagName = config.tagName,
2 | attrName = config.attrName;
3 |
4 | matches.forEach((m) =>
5 | {
6 | // Create a zero-width start tag right before the address
7 | let startTag = addStartTag(tagName, m[0][1], 0);
8 | startTag.setAttribute(attrName, m[0][0]);
9 |
10 | // Create a zero-width end tag right after the address
11 | let endTag = addEndTag(tagName, m[0][1] + m[0][0].length, 0);
12 |
13 | // Pair the tags together
14 | startTag.pairWith(endTag);
15 | });
--------------------------------------------------------------------------------
/src/Plugins/Autoemail/Parser.php:
--------------------------------------------------------------------------------
1 | config['tagName'];
20 | $attrName = $this->config['attrName'];
21 |
22 | foreach ($matches as $m)
23 | {
24 | // Create a zero-width start tag right before the address
25 | $startTag = $this->parser->addStartTag($tagName, $m[0][1], 0);
26 | $startTag->setAttribute($attrName, $m[0][0]);
27 |
28 | // Create a zero-width end tag right after the address
29 | $endTag = $this->parser->addEndTag($tagName, $m[0][1] + strlen($m[0][0]), 0);
30 |
31 | // Pair the tags together
32 | $startTag->pairWith($endTag);
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Plugins/Autoimage/Configurator.php:
--------------------------------------------------------------------------------
1 | attrName . '}"/>';
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Plugins/Autoimage/Parser.js:
--------------------------------------------------------------------------------
1 | const tagPriority = 2;
--------------------------------------------------------------------------------
/src/Plugins/Autoimage/Parser.php:
--------------------------------------------------------------------------------
1 |
2 | {
3 | // Linkify the trimmed URL
4 | linkifyUrl(m[0][1], trimUrl(m[0][0]));
5 | });
6 |
7 | /**
8 | * Linkify given URL at given position
9 | *
10 | * @param {number} tagPos URL's position in the text
11 | * @param {string} url URL
12 | */
13 | function linkifyUrl(tagPos, url)
14 | {
15 | // Create a zero-width end tag right after the URL
16 | let endPos = tagPos + url.length,
17 | endTag = addEndTag(config.tagName, endPos, 0);
18 |
19 | // If the URL starts with "www." we prepend "http://"
20 | if (url[3] === '.')
21 | {
22 | url = 'http://' + url;
23 | }
24 |
25 | // Create a zero-width start tag right before the URL, with a slightly worse priority to
26 | // allow specialized plugins to use the URL instead
27 | let startTag = addStartTag(config.tagName, tagPos, 0, 1);
28 | startTag.setAttribute(config.attrName, url);
29 |
30 | // Pair the tags together
31 | startTag.pairWith(endTag);
32 |
33 | // Protect the tag's content from partial replacements with a low priority tag
34 | let contentTag = addVerbatim(tagPos, endPos - tagPos, 1000);
35 | startTag.cascadeInvalidationTo(contentTag);
36 | }
37 |
38 | /**
39 | * Remove trailing punctuation from given URL
40 | *
41 | * We remove most ASCII non-letters from the end of the string.
42 | * Exceptions:
43 | * - dashes and underscores, (base64 IDs could end with one)
44 | * - equal signs, (because of "foo?bar=")
45 | * - plus signs, (used by some file share services to force download)
46 | * - trailing slashes,
47 | * - closing parentheses. (they are balanced separately)
48 | *
49 | * @param {string} url Original URL
50 | * @return {string} Trimmed URL
51 | */
52 | function trimUrl(url)
53 | {
54 | return url.replace(/(?:(?![-=+)\/_])[\s!-.:-@[-`{-~])+$/, '');
55 | }
--------------------------------------------------------------------------------
/src/Plugins/Autovideo/Configurator.php:
--------------------------------------------------------------------------------
1 | attrName . '}"/>';
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Plugins/Autovideo/Parser.js:
--------------------------------------------------------------------------------
1 | const tagPriority = -1;
--------------------------------------------------------------------------------
/src/Plugins/Autovideo/Parser.php:
--------------------------------------------------------------------------------
1 | bbcodeMonkey = $bbcodeMonkey;
28 | }
29 |
30 | /**
31 | * Normalize a value for storage
32 | *
33 | * @param mixed $value Original value
34 | * @return Repository Normalized value
35 | */
36 | public function normalizeValue($value)
37 | {
38 | return ($value instanceof Repository)
39 | ? $value
40 | : new Repository($value, $this->bbcodeMonkey);
41 | }
42 | }
--------------------------------------------------------------------------------
/src/Plugins/Censor/Parser.js:
--------------------------------------------------------------------------------
1 | let tagName = config.tagName,
2 | attrName = config.attrName;
3 |
4 | matches.forEach((m) =>
5 | {
6 | if (isAllowed(m[0][0]))
7 | {
8 | return;
9 | }
10 |
11 | // NOTE: unlike the PCRE regexp, the JavaScript regexp can consume an extra character at the
12 | // start of the match, so we have to adjust the position and length accordingly
13 | let offset = /^\W/.test(m[0][0]) ? 1 : 0,
14 | word = m[0][0].substring(offset),
15 | tag = addSelfClosingTag(tagName, m[0][1] + offset, word.length);
16 |
17 | if (HINT.CENSOR_HAS_REPLACEMENTS && config.replacements)
18 | {
19 | for (let i = 0; i < config.replacements.length; ++i)
20 | {
21 | let regexp = config.replacements[i][0],
22 | replacement = config.replacements[i][1];
23 |
24 | if (regexp.test(word))
25 | {
26 | tag.setAttribute(attrName, replacement);
27 | break;
28 | }
29 | }
30 | }
31 | });
32 |
33 | /**
34 | * Test whether given word is allowed
35 | *
36 | * @param {string} word
37 | * @return {boolean}
38 | */
39 | function isAllowed(word)
40 | {
41 | return (HINT.CENSOR_HAS_ALLOWED && config.allowed && config.allowed.test(word));
42 | }
--------------------------------------------------------------------------------
/src/Plugins/Censor/Parser.php:
--------------------------------------------------------------------------------
1 | config['tagName'];
20 | $attrName = $this->config['attrName'];
21 | $replacements = $this->config['replacements'] ?? [];
22 | foreach ($matches as $m)
23 | {
24 | if ($this->isAllowed($m[0][0]))
25 | {
26 | continue;
27 | }
28 |
29 | $tag = $this->parser->addSelfClosingTag($tagName, $m[0][1], strlen($m[0][0]));
30 | foreach ($replacements as list($regexp, $replacement))
31 | {
32 | if (preg_match($regexp, $m[0][0]))
33 | {
34 | $tag->setAttribute($attrName, $replacement);
35 | break;
36 | }
37 | }
38 | }
39 | }
40 |
41 | /**
42 | * Test whether given word is allowed
43 | *
44 | * @param string $word
45 | * @return bool
46 | */
47 | protected function isAllowed($word)
48 | {
49 | return (isset($this->config['allowed']) && preg_match($this->config['allowed'], $word));
50 | }
51 | }
--------------------------------------------------------------------------------
/src/Plugins/Emoticons/Configurator/EmoticonCollection.php:
--------------------------------------------------------------------------------
1 |
2 | {
3 | if (HINT.EMOTICONS_NOT_AFTER && config.notAfter && m[0][1] && config.notAfter.test(text[m[0][1] - 1]))
4 | {
5 | return;
6 | }
7 |
8 | addSelfClosingTag(config.tagName, m[0][1], m[0][0].length);
9 | });
--------------------------------------------------------------------------------
/src/Plugins/Emoticons/Parser.php:
--------------------------------------------------------------------------------
1 | parser->addSelfClosingTag($this->config['tagName'], $m[0][1], strlen($m[0][0]));
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/Plugins/Escaper/Configurator.php:
--------------------------------------------------------------------------------
1 | regexp = ($bool) ? '/\\\\./su' : '/\\\\[-!#()*+.:<>@[\\\\\\]^_`{|}~]/';
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | protected function setUp()
44 | {
45 | // Set the default regexp
46 | $this->escapeAll(false);
47 |
48 | // Create the tag
49 | $tag = $this->configurator->tags->add($this->tagName);
50 | $tag->rules->disableAutoLineBreaks();
51 | $tag->rules->ignoreTags();
52 | $tag->rules->preventLineBreaks();
53 | $tag->template = '';
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Plugins/Escaper/Parser.js:
--------------------------------------------------------------------------------
1 | matches.forEach((m) =>
2 | {
3 | addTagPair(
4 | config.tagName,
5 | m[0][1],
6 | 1,
7 | m[0][1] + m[0][0].length,
8 | 0
9 | );
10 | });
--------------------------------------------------------------------------------
/src/Plugins/Escaper/Parser.php:
--------------------------------------------------------------------------------
1 | parser->addTagPair(
22 | $this->config['tagName'],
23 | $m[0][1],
24 | 1,
25 | $m[0][1] + strlen($m[0][0]),
26 | 0
27 | );
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Plugins/HTMLComments/Configurator.php:
--------------------------------------------------------------------------------
1 | /is';
28 |
29 | /**
30 | * @var string Name of the tag used by this plugin
31 | */
32 | protected $tagName = 'HC';
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | protected function setUp()
38 | {
39 | $tag = $this->configurator->tags->add($this->tagName);
40 | $tag->attributes->add($this->attrName);
41 | $tag->rules->ignoreTags();
42 | $tag->template = '';
43 | }
44 | }
--------------------------------------------------------------------------------
/src/Plugins/HTMLComments/Parser.js:
--------------------------------------------------------------------------------
1 | let tagName = config.tagName,
2 | attrName = config.attrName;
3 |
4 | matches.forEach((m) =>
5 | {
6 | // Decode HTML entities
7 | let content = html_entity_decode(m[0][0].substring(4, m[0][0].length - 3));
8 |
9 | // Remove angle brackets from the content
10 | content = content.replace(/[<>]/g, '');
11 |
12 | // Remove trailing dashes
13 | content = content.replace(/-+$/, '');
14 |
15 | // Remove the illegal sequence "--" from the content
16 | content = content.replace(/--/g, '');
17 |
18 | addSelfClosingTag(tagName, m[0][1], m[0][0].length).setAttribute(attrName, content);
19 | });
--------------------------------------------------------------------------------
/src/Plugins/HTMLComments/Parser.php:
--------------------------------------------------------------------------------
1 | config['tagName'];
20 | $attrName = $this->config['attrName'];
21 |
22 | foreach ($matches as $m)
23 | {
24 | // Decode HTML entities
25 | $content = html_entity_decode(substr($m[0][0], 4, -3), ENT_QUOTES, 'UTF-8');
26 |
27 | // Remove angle brackets from the content
28 | $content = str_replace(['<', '>'], '', $content);
29 |
30 | // Remove trailing dashes
31 | $content = rtrim($content, '-');
32 |
33 | // Remove the illegal sequence "--" from the content
34 | $content = str_replace('--', '', $content);
35 |
36 | $this->parser->addSelfClosingTag($tagName, $m[0][1], strlen($m[0][0]))->setAttribute($attrName, $content);
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Plugins/HTMLEntities/Configurator.php:
--------------------------------------------------------------------------------
1 | [a-z]+|#(?>[0-9]+|x[0-9a-f]+));/i';
28 |
29 | /**
30 | * @var string Name of the tag used by this plugin
31 | */
32 | protected $tagName = 'HE';
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | protected function setUp()
38 | {
39 | $tag = $this->configurator->tags->add($this->tagName);
40 | $tag->attributes->add($this->attrName);
41 | $tag->template
42 | = '';
43 | }
44 | }
--------------------------------------------------------------------------------
/src/Plugins/HTMLEntities/Parser.js:
--------------------------------------------------------------------------------
1 | let tagName = config.tagName,
2 | attrName = config.attrName;
3 |
4 | matches.forEach((m) =>
5 | {
6 | let entity = m[0][0],
7 | chr = html_entity_decode(entity);
8 |
9 | if (chr === entity || chr.charCodeAt(0) < 32)
10 | {
11 | // If the entity was not decoded, we assume it's not valid and we ignore it.
12 | // Same thing if it's a control character
13 | return;
14 | }
15 |
16 | addSelfClosingTag(tagName, m[0][1], entity.length).setAttribute(attrName, chr);
17 | });
--------------------------------------------------------------------------------
/src/Plugins/HTMLEntities/Parser.php:
--------------------------------------------------------------------------------
1 | config['tagName'];
20 | $attrName = $this->config['attrName'];
21 |
22 | foreach ($matches as $m)
23 | {
24 | $entity = $m[0][0];
25 | $chr = html_entity_decode($entity, ENT_HTML5 | ENT_QUOTES, 'UTF-8');
26 |
27 | if ($chr === $entity || ord($chr) < 32)
28 | {
29 | // If the entity was not decoded, we assume it's not valid and we ignore it.
30 | // Same thing if it's a control character
31 | continue;
32 | }
33 |
34 | $this->parser->addSelfClosingTag($tagName, $m[0][1], strlen($entity))->setAttribute($attrName, $chr);
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/Plugins/Keywords/Parser.js:
--------------------------------------------------------------------------------
1 | const regexps = config.regexps,
2 | tagName = config.tagName,
3 | attrName = config.attrName;
4 |
5 | let onlyFirst = typeof config.onlyFirst !== 'undefined',
6 | keywords = {};
7 |
8 | regexps.forEach((regexp) =>
9 | {
10 | let m;
11 |
12 | regexp.lastIndex = 0;
13 | while (m = regexp.exec(text))
14 | {
15 | let value = m[0],
16 | pos = m.index;
17 |
18 | if (onlyFirst)
19 | {
20 | if (value in keywords)
21 | {
22 | continue;
23 | }
24 |
25 | keywords[value] = 1;
26 | }
27 |
28 | addSelfClosingTag(tagName, pos, value.length).setAttribute(attrName, value);
29 | }
30 | });
--------------------------------------------------------------------------------
/src/Plugins/Keywords/Parser.php:
--------------------------------------------------------------------------------
1 | config['regexps'];
20 | $tagName = $this->config['tagName'];
21 | $attrName = $this->config['attrName'];
22 |
23 | $onlyFirst = !empty($this->config['onlyFirst']);
24 | $keywords = [];
25 |
26 | foreach ($regexps as $regexp)
27 | {
28 | preg_match_all($regexp, $text, $matches, PREG_OFFSET_CAPTURE);
29 |
30 | foreach ($matches[0] as list($value, $pos))
31 | {
32 | if ($onlyFirst)
33 | {
34 | if (isset($keywords[$value]))
35 | {
36 | continue;
37 | }
38 |
39 | $keywords[$value] = 1;
40 | }
41 |
42 | $this->parser->addSelfClosingTag($tagName, $pos, strlen($value))
43 | ->setAttribute($attrName, $value);
44 | }
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/LinkAttributesSetter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Set a URL or IMG tag's attributes
3 | *
4 | * @param {!Tag} tag URL or IMG tag
5 | * @param {string} linkInfo Link's info: an URL optionally followed by spaces and a title
6 | * @param {string} attrName Name of the URL attribute
7 | */
8 | function setLinkAttributes(tag, linkInfo, attrName)
9 | {
10 | let url = linkInfo.replace(/^\s*/, '').replace(/\s*$/, ''),
11 | title = '',
12 | pos = url.indexOf(' ');
13 | if (pos !== -1)
14 | {
15 | title = url.substring(pos).replace(/^\s*\S/, '').replace(/\S\s*$/, '');
16 | url = url.substring(0, pos);
17 | }
18 | if (/^<.+>$/.test(url))
19 | {
20 | url = url.replace(/^<(.+)>$/, '$1').replace(/\\>/g, '>');
21 | }
22 |
23 | tag.setAttribute(attrName, decode(url));
24 | if (title > '')
25 | {
26 | tag.setAttribute('title', decode(title));
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/LinkAttributesSetter.php:
--------------------------------------------------------------------------------
1 | $/', $url))
33 | {
34 | $url = str_replace('\\>', '>', substr($url, 1, -1));
35 | }
36 |
37 | $tag->setAttribute($attrName, $this->text->decode($url));
38 | if ($title > '')
39 | {
40 | $tag->setAttribute('title', $this->text->decode($title));
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/AbstractInlineMarkup.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Parse given inline markup in text
3 | *
4 | * The markup must start and end with exactly 2 characters
5 | *
6 | * @param {string} str First markup string
7 | * @param {!RegExp} regexp Regexp used to match the markup's span
8 | * @param {string} tagName Name of the tag produced by this markup
9 | */
10 | function parseInlineMarkup(str, regexp, tagName)
11 | {
12 | if (text.indexOf(str) === -1)
13 | {
14 | return;
15 | }
16 |
17 | let m;
18 | while (m = regexp.exec(text))
19 | {
20 | let match = m[0],
21 | matchPos = m.index,
22 | matchLen = match.length,
23 | endPos = matchPos + matchLen - 2;
24 |
25 | addTagPair(tagName, matchPos, 2, endPos, 2);
26 | overwrite(matchPos, 2);
27 | overwrite(endPos, 2);
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/AbstractInlineMarkup.php:
--------------------------------------------------------------------------------
1 | text->indexOf($str);
25 | if ($pos === false)
26 | {
27 | return;
28 | }
29 |
30 | preg_match_all($regexp, $this->text, $matches, PREG_OFFSET_CAPTURE, $pos);
31 | foreach ($matches[0] as [$match, $matchPos])
32 | {
33 | $matchLen = strlen($match);
34 | $endPos = $matchPos + $matchLen - 2;
35 |
36 | $this->parser->addTagPair($tagName, $matchPos, 2, $endPos, 2);
37 | $this->text->overwrite($matchPos, 2);
38 | $this->text->overwrite($endPos, 2);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/AbstractPass.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
32 | $this->text = $text;
33 | }
34 |
35 | /**
36 | * Parse the prepared text from stored parser
37 | *
38 | * @return void
39 | */
40 | abstract public function parse();
41 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/ForcedLineBreaks.js:
--------------------------------------------------------------------------------
1 | function parse()
2 | {
3 | let pos = text.indexOf(" \n");
4 | while (pos > 0)
5 | {
6 | addBrTag(pos + 2).cascadeInvalidationTo(
7 | addVerbatim(pos + 2, 1)
8 | );
9 | pos = text.indexOf(" \n", pos + 3);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/ForcedLineBreaks.php:
--------------------------------------------------------------------------------
1 | text->indexOf(" \n");
18 | while ($pos !== false)
19 | {
20 | $this->parser->addBrTag($pos + 2)->cascadeInvalidationTo(
21 | $this->parser->addVerbatim($pos + 2, 1)
22 | );
23 | $pos = $this->text->indexOf(" \n", $pos + 3);
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/InlineCode.js:
--------------------------------------------------------------------------------
1 | function parse()
2 | {
3 | let markers = getInlineCodeMarkers(),
4 | i = -1,
5 | cnt = markers.length;
6 | while (++i < (cnt - 1))
7 | {
8 | let pos = markers[i].next,
9 | j = i;
10 | if (text[markers[i].pos] !== '`')
11 | {
12 | // Adjust the left marker if its first backtick was escaped
13 | ++markers[i].pos;
14 | --markers[i].len;
15 | }
16 | while (++j < cnt && markers[j].pos === pos)
17 | {
18 | if (markers[j].len === markers[i].len)
19 | {
20 | addInlineCodeTags(markers[i], markers[j]);
21 | i = j;
22 | break;
23 | }
24 | pos = markers[j].next;
25 | }
26 | }
27 | }
28 |
29 | /**
30 | * Add the tag pair for an inline code span
31 | *
32 | * @param {!Object} left Left marker
33 | * @param {!Object} right Right marker
34 | */
35 | function addInlineCodeTags(left, right)
36 | {
37 | let startPos = left.pos,
38 | startLen = left.len + left.trimAfter,
39 | endPos = right.pos - right.trimBefore,
40 | endLen = right.len + right.trimBefore;
41 | addTagPair('C', startPos, startLen, endPos, endLen);
42 | overwrite(startPos, endPos + endLen - startPos);
43 | }
44 |
45 |
46 | /**
47 | * Capture and return inline code markers
48 | *
49 | * @return {!Array}
50 | */
51 | function getInlineCodeMarkers()
52 | {
53 | let pos = text.indexOf('`');
54 | if (pos < 0)
55 | {
56 | return [];
57 | }
58 |
59 | let regexp = /(`+)(\s*)[^\x17`]*/g,
60 | trimNext = 0,
61 | markers = [],
62 | _text = text.replace(/\x1BD/g, '\\`'),
63 | m;
64 | regexp.lastIndex = pos;
65 | while (m = regexp.exec(_text))
66 | {
67 | markers.push({
68 | pos : m.index,
69 | len : m[1].length,
70 | trimBefore : trimNext,
71 | trimAfter : m[2].length,
72 | next : m.index + m[0].length
73 | });
74 | trimNext = m[0].length - m[0].replace(/\s+$/, '').length;
75 | }
76 |
77 | return markers;
78 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/InlineSpoiler.js:
--------------------------------------------------------------------------------
1 | function parse()
2 | {
3 | parseInlineMarkup('>!', />![^\x17]+?!parseInlineMarkup('>!', '/>![^\\x17]+?!', 'ISPOILER');
18 | $this->parseInlineMarkup('||', '/\\|\\|[^\\x17]+?\\|\\|/', 'ISPOILER');
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/LinkReferences.js:
--------------------------------------------------------------------------------
1 | function parse()
2 | {
3 | if (text.indexOf(']:') < 0)
4 | {
5 | return;
6 | }
7 |
8 | let m, regexp = /^\x1A* {0,3}\[([^\x17\]]+)\]: *([^[\s\x17]+ *(?:"[^\x17]*?"|'[^\x17]*?'|\([^\x17)]*\))?) *(?=$|\x17)\n?/gm;
9 | while (m = regexp.exec(text))
10 | {
11 | addIgnoreTag(m.index, m[0].length);
12 |
13 | // Only add the reference if it does not already exist
14 | let id = m[1].toLowerCase();
15 | if (!linkReferences[id])
16 | {
17 | hasReferences = true;
18 | linkReferences[id] = m[2];
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/LinkReferences.php:
--------------------------------------------------------------------------------
1 | text->indexOf(']:') === false)
18 | {
19 | return;
20 | }
21 |
22 | $regexp = '/^\\x1A* {0,3}\\[([^\\x17\\]]+)\\]: *([^[\\s\\x17]+ *(?:"[^\\x17]*?"|\'[^\\x17]*?\'|\\([^\\x17)]*\\))?) *(?=$|\\x17)\\n?/m';
23 | preg_match_all($regexp, $this->text, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
24 | foreach ($matches as $m)
25 | {
26 | $this->parser->addIgnoreTag($m[0][1], strlen($m[0][0]));
27 |
28 | // Only add the reference if it does not already exist
29 | $id = strtolower($m[1][0]);
30 | if (!isset($this->text->linkReferences[$id]))
31 | {
32 | $this->text->hasReferences = true;
33 | $this->text->linkReferences[$id] = $m[2][0];
34 | }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/Strikethrough.js:
--------------------------------------------------------------------------------
1 | function parse()
2 | {
3 | parseInlineMarkup('~~', /~~[^\x17]+?~~(?!~)/g, 'DEL');
4 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/Strikethrough.php:
--------------------------------------------------------------------------------
1 | parseInlineMarkup('~~', '/~~[^\\x17]+?~~(?!~)/', 'DEL');
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/Subscript.js:
--------------------------------------------------------------------------------
1 | function parse()
2 | {
3 | parseAbstractScript('SUB', '~', /~[^\x17\s!"#$%&\'()*+,\-.\/:;<=>?@[\]^_`{}|~]+~?/g, /~\([^\x17()]+\)/g);
4 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/Subscript.php:
--------------------------------------------------------------------------------
1 | parseAbstractScript('SUB', '~', '/~[^\\x17\\s!"#$%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{}|~]++~?/', '/~\\([^\\x17()]++\\)/');
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/Superscript.js:
--------------------------------------------------------------------------------
1 | function parse()
2 | {
3 | parseAbstractScript('SUP', '^', /\^[^\x17\s!"#$%&\'()*+,\-.\/:;<=>?@[\]^_`{}|~]+\^?/g, /\^\([^\x17()]+\)/g);
4 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Passes/Superscript.php:
--------------------------------------------------------------------------------
1 | parseAbstractScript('SUP', '^', '/\\^[^\\x17\\s!"#$%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{}|~]++\\^?/', '/\\^\\([^\\x17()]++\\)/');
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Slugger.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {!Tag} tag
3 | * @param {string} innerText
4 | */
5 | function filterTag(tag, innerText)
6 | {
7 | let slug = innerText.toLowerCase();
8 | slug = slug.replace(/[^a-z0-9]+/g, '-');
9 | slug = slug.replace(/^-/, '').replace(/-$/, '');
10 | if (slug !== '')
11 | {
12 | tag.setAttribute('slug', slug);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Plugins/Litedown/Parser/Slugger.php:
--------------------------------------------------------------------------------
1 | setAttribute('slug', $slug);
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Plugins/MediaEmbed/Configurator/AbstractConfigurableHostHelper.php:
--------------------------------------------------------------------------------
1 | addHosts([$host]);
17 | }
18 |
19 | public function addHosts(array $hosts): void
20 | {
21 | $siteId = $this->getSiteId();
22 | if (!isset($this->configurator->registeredVars['MediaEmbed.sites'][$siteId]))
23 | {
24 | $this->configurator->MediaEmbed->add($siteId);
25 | }
26 |
27 | foreach ($hosts as $host)
28 | {
29 | $host = strtolower($host);
30 | $this->configurator->registeredVars['MediaEmbed.hosts'][$host] = $siteId;
31 | }
32 | }
33 |
34 | public function getHosts(): array
35 | {
36 | $hosts = array_keys(
37 | (array) ($this->configurator->registeredVars['MediaEmbed.hosts'] ?? []),
38 | $this->getSiteId(),
39 | true
40 | );
41 | sort($hosts, SORT_STRING);
42 |
43 | return $hosts;
44 | }
45 |
46 | abstract protected function getSiteId(): string;
47 |
48 | public function setHosts(array $hosts): void
49 | {
50 | $siteId = $this->getSiteId();
51 | if (!isset($this->configurator->registeredVars['MediaEmbed.sites'][$siteId]))
52 | {
53 | $this->configurator->MediaEmbed->add($siteId);
54 | }
55 |
56 | // Remove previously set hosts for this site
57 | foreach ($this->getHosts() as $host)
58 | {
59 | unset($this->configurator->registeredVars['MediaEmbed.hosts'][$host]);
60 | }
61 |
62 | $this->addHosts($hosts);
63 | }
64 | }
--------------------------------------------------------------------------------
/src/Plugins/MediaEmbed/Configurator/SiteHelpers/AbstractSiteHelper.php:
--------------------------------------------------------------------------------
1 | builder))
21 | {
22 | $this->builder = new Builder;
23 | }
24 |
25 | $siteId = $this->getSiteId();
26 | $hosts = $this->getHosts();
27 |
28 | $this->configurator->tags[$siteId]->attributes['embedder']->filterChain[0]->setRegexp(
29 | '/^(?:[-\w]*\.)*' . $this->builder->build($hosts) . '$/'
30 | );
31 | }
32 |
33 | protected function getSiteId(): string
34 | {
35 | return 'bluesky';
36 | }
37 | }
--------------------------------------------------------------------------------
/src/Plugins/MediaEmbed/Configurator/SiteHelpers/MastodonHelper.php:
--------------------------------------------------------------------------------
1 | templateBuilder = $templateBuilder;
28 | }
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | protected function needsWrapper()
34 | {
35 | return false;
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function getContentTemplate()
42 | {
43 | $branches = (isset($this->attributes['when'][0])) ? $this->attributes['when'] : [$this->attributes['when']];
44 | $template = '';
45 | foreach ($branches as $when)
46 | {
47 | $template .= '' . $this->templateBuilder->getTemplate($when) . '';
48 | }
49 | $template .= '' . $this->templateBuilder->getTemplate($this->attributes['otherwise']) . '';
50 |
51 | return $template;
52 | }
53 | }
--------------------------------------------------------------------------------
/src/Plugins/MediaEmbed/Configurator/TemplateGenerators/Flash.php:
--------------------------------------------------------------------------------
1 | $this->attributes['src'],
24 | 'style' => $this->attributes['style'],
25 | 'type' => 'application/x-shockwave-flash',
26 | 'typemustmatch' => ''
27 | ];
28 |
29 | $flashVarsParam = '';
30 | if (isset($this->attributes['flashvars']))
31 | {
32 | $flashVarsParam = $this->generateParamElement('flashvars', $this->attributes['flashvars']);
33 | }
34 |
35 | $template = '';
40 |
41 | return $template;
42 | }
43 |
44 | /**
45 | * Generate a param element to be used inside of an object element
46 | *
47 | * @param string $paramName
48 | * @param string $paramValue
49 | * @return string
50 | */
51 | protected function generateParamElement($paramName, $paramValue)
52 | {
53 | return '' . $this->generateAttributes(['value' => $paramValue]) . '';
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Plugins/MediaEmbed/Configurator/TemplateGenerators/Iframe.php:
--------------------------------------------------------------------------------
1 | '',
19 | 'loading' => 'lazy',
20 | 'scrolling' => 'no',
21 | 'style' => ['border' => '0']
22 | ];
23 |
24 | /**
25 | * @var string[] List of attributes to be passed to the iframe
26 | */
27 | protected $iframeAttributes = ['allow', 'data-s9e-livepreview-ignore-attrs', 'data-s9e-livepreview-onrender', 'onload', 'scrolling', 'src', 'style'];
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function getContentTemplate()
33 | {
34 | $attributes = $this->mergeAttributes($this->defaultIframeAttributes, $this->getFilteredAttributes());
35 |
36 | return '';
37 | }
38 |
39 | /**
40 | * Filter the attributes to keep only those that can be used in an iframe
41 | *
42 | * @return array
43 | */
44 | protected function getFilteredAttributes()
45 | {
46 | return array_intersect_key($this->attributes, array_flip($this->iframeAttributes));
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Plugins/MediaEmbed/Configurator/XenForoHelper.php:
--------------------------------------------------------------------------------
1 |
2 | {
3 | let tagName = config.tagName,
4 | url = m[0][0],
5 | pos = m[0][1],
6 | len = url.length;
7 |
8 | // Give that tag priority over other tags such as Autolink's
9 | addSelfClosingTag(tagName, pos, len, -10).setAttribute('url', url);
10 | });
--------------------------------------------------------------------------------
/src/Plugins/ParserBase.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
33 | $this->config = $config;
34 |
35 | $this->setUp();
36 | }
37 |
38 | /**
39 | * Plugin's setup
40 | *
41 | * @return void
42 | */
43 | protected function setUp()
44 | {
45 | }
46 |
47 | /**
48 | * @param string $text
49 | * @param array $matches If the config array has a "regexp" key, the corresponding matches are
50 | * passed as second parameter. Otherwise, an empty array is passed
51 | * @return void
52 | */
53 | abstract public function parse($text, array $matches);
54 | }
--------------------------------------------------------------------------------
/src/Plugins/Preg/Parser.js:
--------------------------------------------------------------------------------
1 | config.generics.forEach((entry) =>
2 | {
3 | let tagName = entry[0],
4 | regexp = entry[1],
5 | passthroughIdx = entry[2],
6 | map = entry[3],
7 | m;
8 |
9 | // Reset the regexp
10 | regexp.lastIndex = 0;
11 |
12 | while (m = regexp.exec(text))
13 | {
14 | let startTagPos = m.index,
15 | matchLen = m[0].length,
16 | tag;
17 |
18 | if (HINT.PREG_HAS_PASSTHROUGH && passthroughIdx && m[passthroughIdx] !== '')
19 | {
20 | // Compute the position and length of the start tag, end tag, and the content in
21 | // between. m.index gives us the position of the start tag but we don't know its length.
22 | // We use indexOf() to locate the content part so that we know how long the start tag
23 | // is. It is an imperfect solution but it should work well enough in most cases.
24 | let contentPos = text.indexOf(m[passthroughIdx], startTagPos),
25 | contentLen = m[passthroughIdx].length,
26 | startTagLen = contentPos - startTagPos,
27 | endTagPos = contentPos + contentLen,
28 | endTagLen = matchLen - (startTagLen + contentLen);
29 |
30 | tag = addTagPair(tagName, startTagPos, startTagLen, endTagPos, endTagLen, -100);
31 | }
32 | else
33 | {
34 | tag = addSelfClosingTag(tagName, startTagPos, matchLen, -100);
35 | }
36 |
37 | map.forEach((attrName, i) =>
38 | {
39 | // NOTE: subpatterns with no name have an empty entry to preserve the array indices
40 | if (attrName && typeof m[i] !== 'undefined')
41 | {
42 | tag.setAttribute(attrName, m[i]);
43 | }
44 | });
45 | }
46 | });
--------------------------------------------------------------------------------
/src/Plugins/Preg/Parser.php:
--------------------------------------------------------------------------------
1 | config['generics'] as list($tagName, $regexp, $passthroughIdx, $map))
20 | {
21 | preg_match_all($regexp, $text, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
22 |
23 | foreach ($matches as $m)
24 | {
25 | $startTagPos = $m[0][1];
26 | $matchLen = strlen($m[0][0]);
27 |
28 | if ($passthroughIdx && isset($m[$passthroughIdx]) && $m[$passthroughIdx][0] !== '')
29 | {
30 | // Compute the position and length of the start tag, end tag, and the content in
31 | // between. PREG_OFFSET_CAPTURE gives us the position of the content, and we
32 | // know its length. Everything before is considered part of the start tag, and
33 | // everything after is considered part of the end tag
34 | $contentPos = $m[$passthroughIdx][1];
35 | $contentLen = strlen($m[$passthroughIdx][0]);
36 | $startTagLen = $contentPos - $startTagPos;
37 | $endTagPos = $contentPos + $contentLen;
38 | $endTagLen = $matchLen - ($startTagLen + $contentLen);
39 |
40 | $tag = $this->parser->addTagPair($tagName, $startTagPos, $startTagLen, $endTagPos, $endTagLen, -100);
41 | }
42 | else
43 | {
44 | $tag = $this->parser->addSelfClosingTag($tagName, $startTagPos, $matchLen, -100);
45 | }
46 |
47 | foreach ($map as $i => $attrName)
48 | {
49 | if ($attrName && isset($m[$i]) && $m[$i][0] !== '')
50 | {
51 | $tag->setAttribute($attrName, $m[$i][0]);
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/Plugins/TaskLists/filterListItem.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {!Tag} listItem
3 | * @param {string} text
4 | */
5 | function (listItem, text)
6 | {
7 | // Test whether the list item is followed by a task checkbox
8 | let pos = listItem.getPos() + listItem.getLen();
9 | while (text.charAt(pos) === ' ')
10 | {
11 | ++pos;
12 | }
13 | let str = text.substring(pos, pos + 3);
14 | if (!/\[[ Xx]\]/.test(str))
15 | {
16 | return;
17 | }
18 |
19 | // Create a tag for the task and assign it a random ID
20 | let taskId = Math.random().toString(16).substring(2),
21 | taskState = (str === '[ ]') ? 'unchecked' : 'checked',
22 | task = addSelfClosingTag('TASK', pos, 3);
23 |
24 | task.setAttribute('id', taskId);
25 | task.setAttribute('state', taskState);
26 |
27 | listItem.cascadeInvalidationTo(task);
28 | }
--------------------------------------------------------------------------------
/src/Renderers/Unformatted.php:
--------------------------------------------------------------------------------
1 | \n", htmlspecialchars(strip_tags($xml), ENT_COMPAT, 'UTF-8', false));
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Unparser.php:
--------------------------------------------------------------------------------
1 | cacheDir = $cacheDir ?? sys_get_temp_dir();
35 |
36 | return $client;
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Utils/Http/Client.php:
--------------------------------------------------------------------------------
1 | loadXML($xml, LIBXML_NONET);
19 |
20 | return $dom;
21 | }
22 | }
--------------------------------------------------------------------------------