├── CHANGELOG.md ├── LICENSE ├── README.md ├── blueprints.yaml ├── composer.json ├── composer.lock ├── css └── image-captions.css ├── image-captions.php ├── image-captions.yaml └── vendor ├── autoload.php ├── composer ├── ClassLoader.php ├── InstalledVersions.php ├── LICENSE ├── autoload_classmap.php ├── autoload_namespaces.php ├── autoload_psr4.php ├── autoload_real.php ├── autoload_static.php ├── installed.json ├── installed.php └── platform_check.php └── imangazaliev └── didom ├── CHANGELOG.md ├── LICENSE ├── README-RU.md ├── README.md ├── composer.json ├── composer.lock └── src └── DiDom ├── ClassAttribute.php ├── Document.php ├── DocumentFragment.php ├── Element.php ├── Encoder.php ├── Errors.php ├── Exceptions └── InvalidSelectorException.php ├── Node.php ├── Query.php └── StyleAttribute.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.1.0 2 | ## 05/08/2023 3 | 4 | 1. [](#improved) 5 | * Upgraded to latest `v2.0.1` of DiDOM library to support PHP 8.0 6 | 7 | # v2.0.3 8 | ## 12/03/2021 9 | 10 | 1. [](#improved) 11 | * Improved default `scope` to ensure that existing `
` tags are not double-processed 12 | * Alt attribute as the caption source [#15](https://github.com/trilbymedia/grav-plugin-image-captions/issues/15) 13 | * Updated vendor libraries 14 | 15 | # v2.0.2 16 | ## 12/02/2020 17 | 18 | 1. [](#improved) 19 | * Updated vendor libraries 20 | * Use `Page::getRawContent()` rather than `Page::content()` to stop any potential 'looping' 21 | 22 | # v2.0.1 23 | ## 04/23/2019 24 | 25 | 1. [](#improved) 26 | * Updated `didom` library to 1.14.1 27 | 28 | # v2.0.0 29 | ## 01/31/2019 30 | 31 | 1. [](#new) 32 | * Added the ability to override settings per-page [#5](https://github.com/trilbymedia/grav-plugin-image-captions/issues/5) 33 | 1. [](#bugfix) 34 | * Fixed various issues with multiple image captions [#2](https://github.com/trilbymedia/grav-plugin-image-captions/issues/2) 35 | 36 | # v1.0.3 37 | ## 07/27/2018 38 | 39 | 1. [](#improved) 40 | * Remove extra `

` tags surrounding figure that breaks W3C validation [#8](https://github.com/trilbymedia/grav-plugin-image-captions/issues/8) 41 | 1. [](#bugfix) 42 | * Remove extra `` and `` tags from output [#9](https://github.com/trilbymedia/grav-plugin-image-captions/pull/9) 43 | * Extra check for empty content causing out of memory or loop errors [grav#2113](https://github.com/getgrav/grav/issues/2113) 44 | 45 | # v1.0.2 46 | ## 07/13/2018 47 | 48 | 1. [](#bugfix) 49 | * Empty page throws errors [#7](https://github.com/trilbymedia/grav-plugin-image-captions/pull/7) 50 | 51 | # v1.0.1 52 | ## 12/17/2017 53 | 54 | 1. [](#bugfix) 55 | * Empty page throws errors [#1](https://github.com/trilbymedia/grav-plugin-image-captions/issues/1) 56 | 57 | # v1.0.0 58 | ## 11/15/2017 59 | 60 | 1. [](#new) 61 | * ChangeLog started... 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 TrilbyMedia, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image Captions Plugin 2 | 3 | Looks for images with defined title attribute and converts them to figure/figcaption syntax. 4 | 5 | The **Image Captions** Plugin is for [Grav CMS](http://github.com/getgrav/grav). 6 | 7 | This plugin converts the HTML `` tag into a `

` tag with an `
` caption. For example, if you had the following Markdown in your content: 8 | 9 | ```markdown 10 | ![My Image Alt Text](myimage.jpg?classes=caption "My Image Caption") 11 | ``` 12 | 13 | The resulting HTML would be: 14 | 15 | ```html 16 | My Image Alt Text 17 | ``` 18 | 19 | And with the plugin enabled, the result would be: 20 | 21 | ```html 22 |
23 | My Image Alt Text 24 |
My Image Caption
25 |
26 | ``` 27 | 28 | ## Installation 29 | 30 | Installing the Image Captions plugin can be done in one of two ways. The GPM (Grav Package Manager) installation method enables you to quickly and easily install the plugin with a simple terminal command, while the manual method enables you to do so via a zip file. 31 | 32 | ### GPM Installation (Preferred) 33 | 34 | The simplest way to install this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm) through your system's terminal (also called the command line). From the root of your Grav install type: 35 | 36 | bin/gpm install image-captions 37 | 38 | This will install the Image Captions plugin into your `/user/plugins` directory within Grav. Its files can be found under `/your/site/grav/user/plugins/image-captions`. 39 | 40 | ### Manual Installation 41 | 42 | To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `image-captions`. You can find these files on [GitHub](https://github.com/trilbymedia/grav-plugin-image-captions) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras). 43 | 44 | You should now have all the plugin files under 45 | 46 | /your/site/grav/user/plugins/image-captions 47 | 48 | ## Configuration 49 | 50 | Before configuring this plugin, you should copy the `user/plugins/image-captions/image-captions.yaml` to `user/config/plugins/image-captions.yaml` and only edit that copy. 51 | 52 | Here is the default configuration and an explanation of available options: 53 | 54 | ```yaml 55 | enabled: true 56 | ``` 57 | 58 | Enables and disables the plugin. 59 | 60 | ```yaml 61 | built_in_css: true 62 | ``` 63 | 64 | By default the plugin will load some basic css. See below for details 65 | 66 | ```yaml 67 | scope: :not(figure) > img.caption 68 | ``` 69 | 70 | You can define the scope in which plugin will operate. This typically should be something to define the specific images, for example the default is to find all `` tags with class `caption` that are not already inside a `
` tag. You could find all images without a class by simply setting this to `img`. 71 | 72 | ```yaml 73 | entire_page: false 74 | ``` 75 | 76 | By default the plugin will only search the page content during the `onPageProcessed` event. This ensure maximum speed because the results are cached between page loads. However you may have the need to use this plugin on pages where an image is output via Twig or other means, in this case you can set this to `true` and the entire page will be processed on each request. 77 | 78 | ```yaml 79 | figure_class: image-caption 80 | ``` 81 | 82 | You can provide your own class for the `
` tag. 83 | 84 | ```yaml 85 | figcaption_class: 86 | ``` 87 | 88 | You can provide your own class for the `
` tag. 89 | 90 | # Overriding Config Per Page 91 | 92 | You can also override any of the plugin settings on a per-page basis by simply including override values in the page frontmatter. For example: 93 | 94 | ``` 95 | image-captions: 96 | enabled: false 97 | ``` 98 | 99 | Will disable image-captions on this particular page, or: 100 | 101 | ``` 102 | image-captions: 103 | figcaption_class: grav-captions 104 | ``` 105 | 106 | Will set the `figcaption_class` for this particular page. 107 | 108 | # Using the Plugin 109 | 110 | To use the plugin you simply need to ensure your image HTML output matches the scope defined. For example the default scope is simply `img.caption` to you would need to have a title and the `.caption` class: 111 | 112 | ```markdown 113 | ![](myimage.jpg?classes=caption "This is my caption text") 114 | ``` 115 | 116 | # CSS Classes 117 | 118 | The plugin will take any classes set on the original image tag that start with either `caption-` or `figure` and add those to the surrounding `
` tag. The built in CSS provide a few helper CSS classes: 119 | 120 | ```css 121 | .figure-left # float the figure to the left of the content 122 | .figure-right # float the figure to the right of the content 123 | .caption-left # align the caption to the left of the image 124 | .caption-right # align the caption to the right of the image 125 | ``` 126 | 127 | By default both the Figure and the Caption are center aligned 128 | 129 | To use this you can simply put in our markdown: 130 | 131 | ```markdown 132 | ![](myimage.jpg?classes=caption,figure-right "This is my figure floated right and caption text centered") 133 | ``` 134 | 135 | or 136 | 137 | ```markdown 138 | ![](myimage.jpg?classes=caption,figure-left,caption-right "This is my figure floated left with caption text aligned right") 139 | ``` 140 | -------------------------------------------------------------------------------- /blueprints.yaml: -------------------------------------------------------------------------------- 1 | name: Image Captions 2 | version: 2.1.0 3 | type: plugin 4 | slug: image-captions 5 | description: "Looks for images with defined title attribute and converts them to fig-captions." 6 | icon: image 7 | author: 8 | name: Trilby Media, LLC 9 | email: devs@trilby.media 10 | homepage: https://github.com/trilbymedia/grav-plugin-image-captions 11 | keywords: image, caption, figure, figcaption, grav, plugin 12 | bugs: https://github.com/trilbymedia/grav-plugin-image-captions/issues 13 | docs: https://github.com/trilbymedia/grav-plugin-image-captions/blob/develop/README.md 14 | license: MIT 15 | 16 | form: 17 | validation: strict 18 | fields: 19 | enabled: 20 | type: toggle 21 | label: PLUGIN_ADMIN.PLUGIN_STATUS 22 | highlight: 1 23 | default: 0 24 | options: 25 | 1: PLUGIN_ADMIN.ENABLED 26 | 0: PLUGIN_ADMIN.DISABLED 27 | validate: 28 | type: bool 29 | built_in_css: 30 | type: toggle 31 | label: Built-in CSS 32 | highlight: 1 33 | default: 0 34 | options: 35 | 1: PLUGIN_ADMIN.ENABLED 36 | 0: PLUGIN_ADMIN.DISABLED 37 | validate: 38 | type: bool 39 | entire_page: 40 | type: toggle 41 | label: Scan the entire page 42 | help: With this disabled, only the content is scanned 43 | highlight: 0 44 | default: 0 45 | options: 46 | 1: PLUGIN_ADMIN.ENABLED 47 | 0: PLUGIN_ADMIN.DISABLED 48 | validate: 49 | type: bool 50 | 51 | source: 52 | type: select 53 | label: Source 54 | size: small 55 | default: title 56 | help: Defines which attribute will use as the caption source - title or alt 57 | options: 58 | title: Title 59 | alt: Alt 60 | 61 | scope: 62 | type: text 63 | label: Scope 64 | placeholder: img.caption 65 | help: img.caption will search for tags with class='caption' 66 | 67 | figure_class: 68 | type: text 69 | label: Figure class 70 | placeholder: image-caption 71 | 72 | figcaption_class: 73 | type: text 74 | label: Figcaption class 75 | 76 | 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "imangazaliev/didom": "^2.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "be2707bcb2a99562819b97a30380d0e3", 8 | "packages": [ 9 | { 10 | "name": "imangazaliev/didom", 11 | "version": "2.0.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/Imangazaliev/DiDOM.git", 15 | "reference": "50fa6595d14f22c0c984efed5c818485cf548136" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/Imangazaliev/DiDOM/zipball/50fa6595d14f22c0c984efed5c818485cf548136", 20 | "reference": "50fa6595d14f22c0c984efed5c818485cf548136", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-dom": "*", 25 | "ext-iconv": "*", 26 | "php": ">=7.2" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^8.5" 30 | }, 31 | "type": "library", 32 | "autoload": { 33 | "psr-4": { 34 | "DiDom\\": "src/DiDom/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "Imangazaliev Muhammad", 44 | "email": "imangazalievm@gmail.com" 45 | } 46 | ], 47 | "description": "Simple and fast HTML parser", 48 | "homepage": "https://github.com/Imangazaliev/DiDOM", 49 | "keywords": [ 50 | "didom", 51 | "html", 52 | "parser", 53 | "xml" 54 | ], 55 | "support": { 56 | "issues": "https://github.com/Imangazaliev/DiDOM/issues", 57 | "source": "https://github.com/Imangazaliev/DiDOM/tree/2.0.1" 58 | }, 59 | "time": "2023-03-05T03:23:48+00:00" 60 | } 61 | ], 62 | "packages-dev": [], 63 | "aliases": [], 64 | "minimum-stability": "stable", 65 | "stability-flags": [], 66 | "prefer-stable": false, 67 | "prefer-lowest": false, 68 | "platform": [], 69 | "platform-dev": [], 70 | "plugin-api-version": "2.3.0" 71 | } 72 | -------------------------------------------------------------------------------- /css/image-captions.css: -------------------------------------------------------------------------------- 1 | figure.image-caption { 2 | text-align: center; 3 | margin: 1rem; 4 | } 5 | 6 | figure.image-caption figcaption { 7 | opacity: 0.7; 8 | font-size: 90%; 9 | } 10 | 11 | figure.figure-right { 12 | float: right; 13 | margin-right: 0; 14 | } 15 | 16 | figure.figure-left { 17 | float: left; 18 | margin-left: 0; 19 | } 20 | 21 | figure.caption-left figcaption { 22 | text-align: left; 23 | } 24 | 25 | figure.caption-right figcaption { 26 | text-align: right; 27 | } -------------------------------------------------------------------------------- /image-captions.php: -------------------------------------------------------------------------------- 1 | ['onPluginsInitialized', 0] 22 | ]; 23 | } 24 | 25 | /** 26 | * Initialize the plugin 27 | */ 28 | public function onPluginsInitialized() 29 | { 30 | // Don't proceed if we are in the admin plugin 31 | if ($this->isAdmin()) { 32 | return; 33 | } 34 | 35 | include __DIR__.'/vendor/autoload.php'; 36 | 37 | if ($this->grav['config']->get('plugins.image-captions.entire_page')) { 38 | $this->enable([ 39 | 'onOutputGenerated' => ['onOutputGenerated', 0], 40 | 'onTwigSiteVariables' => ['onTwigSiteVariables', 0] 41 | ]); 42 | } else { 43 | $this->enable([ 44 | 'onPageContentProcessed' => ['onPageContentProcessed', 0], 45 | 'onTwigSiteVariables' => ['onTwigSiteVariables', 0] 46 | ]); 47 | } 48 | 49 | 50 | } 51 | 52 | /** 53 | * Process on entire Grav output 54 | */ 55 | public function onOutputGenerated() 56 | { 57 | $page = $this->grav['page']; 58 | $this->updateConfig($page); 59 | 60 | if ($this->config->get('plugins.image-captions.enabled') === false) { 61 | return; 62 | } 63 | 64 | $this->grav->output = $this->processFigures($this->grav->output); 65 | } 66 | 67 | /** 68 | * Process on page content 69 | * 70 | * @param Event $event 71 | */ 72 | public function onPageContentProcessed(Event $event) 73 | { 74 | $page = $event['page']; 75 | $this->updateConfig($page); 76 | 77 | if ($this->config->get('plugins.image-captions.enabled') === false) { 78 | return; 79 | } 80 | 81 | $content = $page->getRawContent(); 82 | $content = $this->processFigures($content); 83 | $page->setRawContent($content); 84 | } 85 | 86 | /** 87 | * Load the CSS if configured 88 | */ 89 | public function onTwigSiteVariables() 90 | { 91 | 92 | if ($this->config->get('plugins.image-captions.built_in_css')) { 93 | $this->grav['assets']->add('plugin://image-captions/css/image-captions.css'); 94 | } 95 | } 96 | 97 | /** 98 | * Process content and replace any items in scope with the figure/figcaption structure 99 | * 100 | * @param $content 101 | * @return string 102 | */ 103 | protected function processFigures($content) 104 | { 105 | // Check for empty content 106 | if (strlen($content) === 0) { 107 | return ''; 108 | } 109 | 110 | $document = new Document($content); 111 | 112 | $scope = trim($this->grav['config']->get('plugins.image-captions.scope')); 113 | $figure_class = $this->grav['config']->get('plugins.image-captions.figure_class', ''); 114 | $figcaption_class = $this->grav['config']->get('plugins.image-captions.figcaption_class', ''); 115 | $source = $this->grav['config']->get('plugins.image-captions.source'); 116 | 117 | if (count($images = $document->find($scope)) > 0) { 118 | foreach ($images as $image) { 119 | if ($source == 'alt') { 120 | $caption = $image->getAttribute('alt'); 121 | } else { 122 | $caption = $image->getAttribute('title'); 123 | } 124 | 125 | if ($caption) { 126 | $figure_classes = [$figure_class]; 127 | 128 | // If there are any `caption-*` classes on the image, add them to the figure 129 | $image_classes = explode(' ', $image->getAttribute('class')); 130 | foreach ($image_classes as $class) { 131 | if (preg_match('/^(caption-|figure-).*/', $class)) { 132 | $figure_classes[] = $class; 133 | } 134 | } 135 | 136 | $figcaption = new Element('figcaption',$caption, ['class' => $figcaption_class]); 137 | $items = [$image, $figcaption]; 138 | $figure = new Element('figure', '', ['class' => implode(' ', $figure_classes)]); 139 | $figure->appendChild($items); 140 | $image->replace($figure); 141 | } 142 | } 143 | return $this->cleanupTags($document->html()); 144 | } 145 | 146 | return $content; 147 | } 148 | 149 | private function updateConfig($page) 150 | { 151 | $config = $this->mergeConfig($page)->toArray(); 152 | $this->config->set('plugins.image-captions', $config); 153 | } 154 | 155 | /** 156 | * Removes html and body tags at the begining and end of the html source 157 | * 158 | * @param $html 159 | * @return string 160 | */ 161 | private function cleanupTags($html) 162 | { 163 | // remove html/body tags 164 | $html = preg_replace('##', '', $html); 165 | $html = preg_replace('##', '', $html); 166 | 167 | // remove whitespace 168 | $html = trim($html); 169 | 170 | // remove p tags 171 | preg_match_all('#

(?:\s*)(()?.*)(?:\s*)()(?:\s*)(<\/a>)?(?:\s*)<\/p>#m', $html, $matches); 172 | 173 | if (is_array($matches) && !empty($matches)) { 174 | $num_matches = count($matches[0]); 175 | for ($i = 0; $i < $num_matches; $i++) { 176 | $original = $matches[0][$i]; 177 | $new = $matches[1][$i] . $matches[3][$i] . $matches[5][$i]; 178 | 179 | $html = str_replace($original, $new, $html); 180 | } 181 | } 182 | 183 | return $html; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /image-captions.yaml: -------------------------------------------------------------------------------- 1 | enabled: true 2 | built_in_css: true 3 | entire_page: false 4 | source: title 5 | scope: :not(figure) > img.caption 6 | figure_class: image-caption 7 | figcaption_class: 8 | -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see https://www.php-fig.org/psr/psr-0/ 41 | * @see https://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | /** @var \Closure(string):void */ 46 | private static $includeFile; 47 | 48 | /** @var ?string */ 49 | private $vendorDir; 50 | 51 | // PSR-4 52 | /** 53 | * @var array[] 54 | * @psalm-var array> 55 | */ 56 | private $prefixLengthsPsr4 = array(); 57 | /** 58 | * @var array[] 59 | * @psalm-var array> 60 | */ 61 | private $prefixDirsPsr4 = array(); 62 | /** 63 | * @var array[] 64 | * @psalm-var array 65 | */ 66 | private $fallbackDirsPsr4 = array(); 67 | 68 | // PSR-0 69 | /** 70 | * @var array[] 71 | * @psalm-var array> 72 | */ 73 | private $prefixesPsr0 = array(); 74 | /** 75 | * @var array[] 76 | * @psalm-var array 77 | */ 78 | private $fallbackDirsPsr0 = array(); 79 | 80 | /** @var bool */ 81 | private $useIncludePath = false; 82 | 83 | /** 84 | * @var string[] 85 | * @psalm-var array 86 | */ 87 | private $classMap = array(); 88 | 89 | /** @var bool */ 90 | private $classMapAuthoritative = false; 91 | 92 | /** 93 | * @var bool[] 94 | * @psalm-var array 95 | */ 96 | private $missingClasses = array(); 97 | 98 | /** @var ?string */ 99 | private $apcuPrefix; 100 | 101 | /** 102 | * @var self[] 103 | */ 104 | private static $registeredLoaders = array(); 105 | 106 | /** 107 | * @param ?string $vendorDir 108 | */ 109 | public function __construct($vendorDir = null) 110 | { 111 | $this->vendorDir = $vendorDir; 112 | self::initializeIncludeClosure(); 113 | } 114 | 115 | /** 116 | * @return string[] 117 | */ 118 | public function getPrefixes() 119 | { 120 | if (!empty($this->prefixesPsr0)) { 121 | return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); 122 | } 123 | 124 | return array(); 125 | } 126 | 127 | /** 128 | * @return array[] 129 | * @psalm-return array> 130 | */ 131 | public function getPrefixesPsr4() 132 | { 133 | return $this->prefixDirsPsr4; 134 | } 135 | 136 | /** 137 | * @return array[] 138 | * @psalm-return array 139 | */ 140 | public function getFallbackDirs() 141 | { 142 | return $this->fallbackDirsPsr0; 143 | } 144 | 145 | /** 146 | * @return array[] 147 | * @psalm-return array 148 | */ 149 | public function getFallbackDirsPsr4() 150 | { 151 | return $this->fallbackDirsPsr4; 152 | } 153 | 154 | /** 155 | * @return string[] Array of classname => path 156 | * @psalm-return array 157 | */ 158 | public function getClassMap() 159 | { 160 | return $this->classMap; 161 | } 162 | 163 | /** 164 | * @param string[] $classMap Class to filename map 165 | * @psalm-param array $classMap 166 | * 167 | * @return void 168 | */ 169 | public function addClassMap(array $classMap) 170 | { 171 | if ($this->classMap) { 172 | $this->classMap = array_merge($this->classMap, $classMap); 173 | } else { 174 | $this->classMap = $classMap; 175 | } 176 | } 177 | 178 | /** 179 | * Registers a set of PSR-0 directories for a given prefix, either 180 | * appending or prepending to the ones previously set for this prefix. 181 | * 182 | * @param string $prefix The prefix 183 | * @param string[]|string $paths The PSR-0 root directories 184 | * @param bool $prepend Whether to prepend the directories 185 | * 186 | * @return void 187 | */ 188 | public function add($prefix, $paths, $prepend = false) 189 | { 190 | if (!$prefix) { 191 | if ($prepend) { 192 | $this->fallbackDirsPsr0 = array_merge( 193 | (array) $paths, 194 | $this->fallbackDirsPsr0 195 | ); 196 | } else { 197 | $this->fallbackDirsPsr0 = array_merge( 198 | $this->fallbackDirsPsr0, 199 | (array) $paths 200 | ); 201 | } 202 | 203 | return; 204 | } 205 | 206 | $first = $prefix[0]; 207 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 208 | $this->prefixesPsr0[$first][$prefix] = (array) $paths; 209 | 210 | return; 211 | } 212 | if ($prepend) { 213 | $this->prefixesPsr0[$first][$prefix] = array_merge( 214 | (array) $paths, 215 | $this->prefixesPsr0[$first][$prefix] 216 | ); 217 | } else { 218 | $this->prefixesPsr0[$first][$prefix] = array_merge( 219 | $this->prefixesPsr0[$first][$prefix], 220 | (array) $paths 221 | ); 222 | } 223 | } 224 | 225 | /** 226 | * Registers a set of PSR-4 directories for a given namespace, either 227 | * appending or prepending to the ones previously set for this namespace. 228 | * 229 | * @param string $prefix The prefix/namespace, with trailing '\\' 230 | * @param string[]|string $paths The PSR-4 base directories 231 | * @param bool $prepend Whether to prepend the directories 232 | * 233 | * @throws \InvalidArgumentException 234 | * 235 | * @return void 236 | */ 237 | public function addPsr4($prefix, $paths, $prepend = false) 238 | { 239 | if (!$prefix) { 240 | // Register directories for the root namespace. 241 | if ($prepend) { 242 | $this->fallbackDirsPsr4 = array_merge( 243 | (array) $paths, 244 | $this->fallbackDirsPsr4 245 | ); 246 | } else { 247 | $this->fallbackDirsPsr4 = array_merge( 248 | $this->fallbackDirsPsr4, 249 | (array) $paths 250 | ); 251 | } 252 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 253 | // Register directories for a new namespace. 254 | $length = strlen($prefix); 255 | if ('\\' !== $prefix[$length - 1]) { 256 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 257 | } 258 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 259 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 260 | } elseif ($prepend) { 261 | // Prepend directories for an already registered namespace. 262 | $this->prefixDirsPsr4[$prefix] = array_merge( 263 | (array) $paths, 264 | $this->prefixDirsPsr4[$prefix] 265 | ); 266 | } else { 267 | // Append directories for an already registered namespace. 268 | $this->prefixDirsPsr4[$prefix] = array_merge( 269 | $this->prefixDirsPsr4[$prefix], 270 | (array) $paths 271 | ); 272 | } 273 | } 274 | 275 | /** 276 | * Registers a set of PSR-0 directories for a given prefix, 277 | * replacing any others previously set for this prefix. 278 | * 279 | * @param string $prefix The prefix 280 | * @param string[]|string $paths The PSR-0 base directories 281 | * 282 | * @return void 283 | */ 284 | public function set($prefix, $paths) 285 | { 286 | if (!$prefix) { 287 | $this->fallbackDirsPsr0 = (array) $paths; 288 | } else { 289 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 290 | } 291 | } 292 | 293 | /** 294 | * Registers a set of PSR-4 directories for a given namespace, 295 | * replacing any others previously set for this namespace. 296 | * 297 | * @param string $prefix The prefix/namespace, with trailing '\\' 298 | * @param string[]|string $paths The PSR-4 base directories 299 | * 300 | * @throws \InvalidArgumentException 301 | * 302 | * @return void 303 | */ 304 | public function setPsr4($prefix, $paths) 305 | { 306 | if (!$prefix) { 307 | $this->fallbackDirsPsr4 = (array) $paths; 308 | } else { 309 | $length = strlen($prefix); 310 | if ('\\' !== $prefix[$length - 1]) { 311 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 312 | } 313 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 314 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 315 | } 316 | } 317 | 318 | /** 319 | * Turns on searching the include path for class files. 320 | * 321 | * @param bool $useIncludePath 322 | * 323 | * @return void 324 | */ 325 | public function setUseIncludePath($useIncludePath) 326 | { 327 | $this->useIncludePath = $useIncludePath; 328 | } 329 | 330 | /** 331 | * Can be used to check if the autoloader uses the include path to check 332 | * for classes. 333 | * 334 | * @return bool 335 | */ 336 | public function getUseIncludePath() 337 | { 338 | return $this->useIncludePath; 339 | } 340 | 341 | /** 342 | * Turns off searching the prefix and fallback directories for classes 343 | * that have not been registered with the class map. 344 | * 345 | * @param bool $classMapAuthoritative 346 | * 347 | * @return void 348 | */ 349 | public function setClassMapAuthoritative($classMapAuthoritative) 350 | { 351 | $this->classMapAuthoritative = $classMapAuthoritative; 352 | } 353 | 354 | /** 355 | * Should class lookup fail if not found in the current class map? 356 | * 357 | * @return bool 358 | */ 359 | public function isClassMapAuthoritative() 360 | { 361 | return $this->classMapAuthoritative; 362 | } 363 | 364 | /** 365 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 366 | * 367 | * @param string|null $apcuPrefix 368 | * 369 | * @return void 370 | */ 371 | public function setApcuPrefix($apcuPrefix) 372 | { 373 | $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; 374 | } 375 | 376 | /** 377 | * The APCu prefix in use, or null if APCu caching is not enabled. 378 | * 379 | * @return string|null 380 | */ 381 | public function getApcuPrefix() 382 | { 383 | return $this->apcuPrefix; 384 | } 385 | 386 | /** 387 | * Registers this instance as an autoloader. 388 | * 389 | * @param bool $prepend Whether to prepend the autoloader or not 390 | * 391 | * @return void 392 | */ 393 | public function register($prepend = false) 394 | { 395 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 396 | 397 | if (null === $this->vendorDir) { 398 | return; 399 | } 400 | 401 | if ($prepend) { 402 | self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; 403 | } else { 404 | unset(self::$registeredLoaders[$this->vendorDir]); 405 | self::$registeredLoaders[$this->vendorDir] = $this; 406 | } 407 | } 408 | 409 | /** 410 | * Unregisters this instance as an autoloader. 411 | * 412 | * @return void 413 | */ 414 | public function unregister() 415 | { 416 | spl_autoload_unregister(array($this, 'loadClass')); 417 | 418 | if (null !== $this->vendorDir) { 419 | unset(self::$registeredLoaders[$this->vendorDir]); 420 | } 421 | } 422 | 423 | /** 424 | * Loads the given class or interface. 425 | * 426 | * @param string $class The name of the class 427 | * @return true|null True if loaded, null otherwise 428 | */ 429 | public function loadClass($class) 430 | { 431 | if ($file = $this->findFile($class)) { 432 | $includeFile = self::$includeFile; 433 | $includeFile($file); 434 | 435 | return true; 436 | } 437 | 438 | return null; 439 | } 440 | 441 | /** 442 | * Finds the path to the file where the class is defined. 443 | * 444 | * @param string $class The name of the class 445 | * 446 | * @return string|false The path if found, false otherwise 447 | */ 448 | public function findFile($class) 449 | { 450 | // class map lookup 451 | if (isset($this->classMap[$class])) { 452 | return $this->classMap[$class]; 453 | } 454 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 455 | return false; 456 | } 457 | if (null !== $this->apcuPrefix) { 458 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 459 | if ($hit) { 460 | return $file; 461 | } 462 | } 463 | 464 | $file = $this->findFileWithExtension($class, '.php'); 465 | 466 | // Search for Hack files if we are running on HHVM 467 | if (false === $file && defined('HHVM_VERSION')) { 468 | $file = $this->findFileWithExtension($class, '.hh'); 469 | } 470 | 471 | if (null !== $this->apcuPrefix) { 472 | apcu_add($this->apcuPrefix.$class, $file); 473 | } 474 | 475 | if (false === $file) { 476 | // Remember that this class does not exist. 477 | $this->missingClasses[$class] = true; 478 | } 479 | 480 | return $file; 481 | } 482 | 483 | /** 484 | * Returns the currently registered loaders indexed by their corresponding vendor directories. 485 | * 486 | * @return self[] 487 | */ 488 | public static function getRegisteredLoaders() 489 | { 490 | return self::$registeredLoaders; 491 | } 492 | 493 | /** 494 | * @param string $class 495 | * @param string $ext 496 | * @return string|false 497 | */ 498 | private function findFileWithExtension($class, $ext) 499 | { 500 | // PSR-4 lookup 501 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 502 | 503 | $first = $class[0]; 504 | if (isset($this->prefixLengthsPsr4[$first])) { 505 | $subPath = $class; 506 | while (false !== $lastPos = strrpos($subPath, '\\')) { 507 | $subPath = substr($subPath, 0, $lastPos); 508 | $search = $subPath . '\\'; 509 | if (isset($this->prefixDirsPsr4[$search])) { 510 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 511 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 512 | if (file_exists($file = $dir . $pathEnd)) { 513 | return $file; 514 | } 515 | } 516 | } 517 | } 518 | } 519 | 520 | // PSR-4 fallback dirs 521 | foreach ($this->fallbackDirsPsr4 as $dir) { 522 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 523 | return $file; 524 | } 525 | } 526 | 527 | // PSR-0 lookup 528 | if (false !== $pos = strrpos($class, '\\')) { 529 | // namespaced class name 530 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 531 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 532 | } else { 533 | // PEAR-like class name 534 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 535 | } 536 | 537 | if (isset($this->prefixesPsr0[$first])) { 538 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 539 | if (0 === strpos($class, $prefix)) { 540 | foreach ($dirs as $dir) { 541 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 542 | return $file; 543 | } 544 | } 545 | } 546 | } 547 | } 548 | 549 | // PSR-0 fallback dirs 550 | foreach ($this->fallbackDirsPsr0 as $dir) { 551 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 552 | return $file; 553 | } 554 | } 555 | 556 | // PSR-0 include paths. 557 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 558 | return $file; 559 | } 560 | 561 | return false; 562 | } 563 | 564 | /** 565 | * @return void 566 | */ 567 | private static function initializeIncludeClosure() 568 | { 569 | if (self::$includeFile !== null) { 570 | return; 571 | } 572 | 573 | /** 574 | * Scope isolated include. 575 | * 576 | * Prevents access to $this/self from included files. 577 | * 578 | * @param string $file 579 | * @return void 580 | */ 581 | self::$includeFile = \Closure::bind(static function($file) { 582 | include $file; 583 | }, null, null); 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /vendor/composer/InstalledVersions.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer; 14 | 15 | use Composer\Autoload\ClassLoader; 16 | use Composer\Semver\VersionParser; 17 | 18 | /** 19 | * This class is copied in every Composer installed project and available to all 20 | * 21 | * See also https://getcomposer.org/doc/07-runtime.md#installed-versions 22 | * 23 | * To require its presence, you can require `composer-runtime-api ^2.0` 24 | * 25 | * @final 26 | */ 27 | class InstalledVersions 28 | { 29 | /** 30 | * @var mixed[]|null 31 | * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null 32 | */ 33 | private static $installed; 34 | 35 | /** 36 | * @var bool|null 37 | */ 38 | private static $canGetVendors; 39 | 40 | /** 41 | * @var array[] 42 | * @psalm-var array}> 43 | */ 44 | private static $installedByVendor = array(); 45 | 46 | /** 47 | * Returns a list of all package names which are present, either by being installed, replaced or provided 48 | * 49 | * @return string[] 50 | * @psalm-return list 51 | */ 52 | public static function getInstalledPackages() 53 | { 54 | $packages = array(); 55 | foreach (self::getInstalled() as $installed) { 56 | $packages[] = array_keys($installed['versions']); 57 | } 58 | 59 | if (1 === \count($packages)) { 60 | return $packages[0]; 61 | } 62 | 63 | return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); 64 | } 65 | 66 | /** 67 | * Returns a list of all package names with a specific type e.g. 'library' 68 | * 69 | * @param string $type 70 | * @return string[] 71 | * @psalm-return list 72 | */ 73 | public static function getInstalledPackagesByType($type) 74 | { 75 | $packagesByType = array(); 76 | 77 | foreach (self::getInstalled() as $installed) { 78 | foreach ($installed['versions'] as $name => $package) { 79 | if (isset($package['type']) && $package['type'] === $type) { 80 | $packagesByType[] = $name; 81 | } 82 | } 83 | } 84 | 85 | return $packagesByType; 86 | } 87 | 88 | /** 89 | * Checks whether the given package is installed 90 | * 91 | * This also returns true if the package name is provided or replaced by another package 92 | * 93 | * @param string $packageName 94 | * @param bool $includeDevRequirements 95 | * @return bool 96 | */ 97 | public static function isInstalled($packageName, $includeDevRequirements = true) 98 | { 99 | foreach (self::getInstalled() as $installed) { 100 | if (isset($installed['versions'][$packageName])) { 101 | return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; 102 | } 103 | } 104 | 105 | return false; 106 | } 107 | 108 | /** 109 | * Checks whether the given package satisfies a version constraint 110 | * 111 | * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: 112 | * 113 | * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') 114 | * 115 | * @param VersionParser $parser Install composer/semver to have access to this class and functionality 116 | * @param string $packageName 117 | * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package 118 | * @return bool 119 | */ 120 | public static function satisfies(VersionParser $parser, $packageName, $constraint) 121 | { 122 | $constraint = $parser->parseConstraints((string) $constraint); 123 | $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); 124 | 125 | return $provided->matches($constraint); 126 | } 127 | 128 | /** 129 | * Returns a version constraint representing all the range(s) which are installed for a given package 130 | * 131 | * It is easier to use this via isInstalled() with the $constraint argument if you need to check 132 | * whether a given version of a package is installed, and not just whether it exists 133 | * 134 | * @param string $packageName 135 | * @return string Version constraint usable with composer/semver 136 | */ 137 | public static function getVersionRanges($packageName) 138 | { 139 | foreach (self::getInstalled() as $installed) { 140 | if (!isset($installed['versions'][$packageName])) { 141 | continue; 142 | } 143 | 144 | $ranges = array(); 145 | if (isset($installed['versions'][$packageName]['pretty_version'])) { 146 | $ranges[] = $installed['versions'][$packageName]['pretty_version']; 147 | } 148 | if (array_key_exists('aliases', $installed['versions'][$packageName])) { 149 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); 150 | } 151 | if (array_key_exists('replaced', $installed['versions'][$packageName])) { 152 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); 153 | } 154 | if (array_key_exists('provided', $installed['versions'][$packageName])) { 155 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); 156 | } 157 | 158 | return implode(' || ', $ranges); 159 | } 160 | 161 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 162 | } 163 | 164 | /** 165 | * @param string $packageName 166 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 167 | */ 168 | public static function getVersion($packageName) 169 | { 170 | foreach (self::getInstalled() as $installed) { 171 | if (!isset($installed['versions'][$packageName])) { 172 | continue; 173 | } 174 | 175 | if (!isset($installed['versions'][$packageName]['version'])) { 176 | return null; 177 | } 178 | 179 | return $installed['versions'][$packageName]['version']; 180 | } 181 | 182 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 183 | } 184 | 185 | /** 186 | * @param string $packageName 187 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 188 | */ 189 | public static function getPrettyVersion($packageName) 190 | { 191 | foreach (self::getInstalled() as $installed) { 192 | if (!isset($installed['versions'][$packageName])) { 193 | continue; 194 | } 195 | 196 | if (!isset($installed['versions'][$packageName]['pretty_version'])) { 197 | return null; 198 | } 199 | 200 | return $installed['versions'][$packageName]['pretty_version']; 201 | } 202 | 203 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 204 | } 205 | 206 | /** 207 | * @param string $packageName 208 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference 209 | */ 210 | public static function getReference($packageName) 211 | { 212 | foreach (self::getInstalled() as $installed) { 213 | if (!isset($installed['versions'][$packageName])) { 214 | continue; 215 | } 216 | 217 | if (!isset($installed['versions'][$packageName]['reference'])) { 218 | return null; 219 | } 220 | 221 | return $installed['versions'][$packageName]['reference']; 222 | } 223 | 224 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 225 | } 226 | 227 | /** 228 | * @param string $packageName 229 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. 230 | */ 231 | public static function getInstallPath($packageName) 232 | { 233 | foreach (self::getInstalled() as $installed) { 234 | if (!isset($installed['versions'][$packageName])) { 235 | continue; 236 | } 237 | 238 | return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; 239 | } 240 | 241 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 242 | } 243 | 244 | /** 245 | * @return array 246 | * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} 247 | */ 248 | public static function getRootPackage() 249 | { 250 | $installed = self::getInstalled(); 251 | 252 | return $installed[0]['root']; 253 | } 254 | 255 | /** 256 | * Returns the raw installed.php data for custom implementations 257 | * 258 | * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. 259 | * @return array[] 260 | * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} 261 | */ 262 | public static function getRawData() 263 | { 264 | @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); 265 | 266 | if (null === self::$installed) { 267 | // only require the installed.php file if this file is loaded from its dumped location, 268 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 269 | if (substr(__DIR__, -8, 1) !== 'C') { 270 | self::$installed = include __DIR__ . '/installed.php'; 271 | } else { 272 | self::$installed = array(); 273 | } 274 | } 275 | 276 | return self::$installed; 277 | } 278 | 279 | /** 280 | * Returns the raw data of all installed.php which are currently loaded for custom implementations 281 | * 282 | * @return array[] 283 | * @psalm-return list}> 284 | */ 285 | public static function getAllRawData() 286 | { 287 | return self::getInstalled(); 288 | } 289 | 290 | /** 291 | * Lets you reload the static array from another file 292 | * 293 | * This is only useful for complex integrations in which a project needs to use 294 | * this class but then also needs to execute another project's autoloader in process, 295 | * and wants to ensure both projects have access to their version of installed.php. 296 | * 297 | * A typical case would be PHPUnit, where it would need to make sure it reads all 298 | * the data it needs from this class, then call reload() with 299 | * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure 300 | * the project in which it runs can then also use this class safely, without 301 | * interference between PHPUnit's dependencies and the project's dependencies. 302 | * 303 | * @param array[] $data A vendor/composer/installed.php data set 304 | * @return void 305 | * 306 | * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data 307 | */ 308 | public static function reload($data) 309 | { 310 | self::$installed = $data; 311 | self::$installedByVendor = array(); 312 | } 313 | 314 | /** 315 | * @return array[] 316 | * @psalm-return list}> 317 | */ 318 | private static function getInstalled() 319 | { 320 | if (null === self::$canGetVendors) { 321 | self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); 322 | } 323 | 324 | $installed = array(); 325 | 326 | if (self::$canGetVendors) { 327 | foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { 328 | if (isset(self::$installedByVendor[$vendorDir])) { 329 | $installed[] = self::$installedByVendor[$vendorDir]; 330 | } elseif (is_file($vendorDir.'/composer/installed.php')) { 331 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 332 | $required = require $vendorDir.'/composer/installed.php'; 333 | $installed[] = self::$installedByVendor[$vendorDir] = $required; 334 | if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { 335 | self::$installed = $installed[count($installed) - 1]; 336 | } 337 | } 338 | } 339 | } 340 | 341 | if (null === self::$installed) { 342 | // only require the installed.php file if this file is loaded from its dumped location, 343 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 344 | if (substr(__DIR__, -8, 1) !== 'C') { 345 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 346 | $required = require __DIR__ . '/installed.php'; 347 | self::$installed = $required; 348 | } else { 349 | self::$installed = array(); 350 | } 351 | } 352 | 353 | if (self::$installed !== array()) { 354 | $installed[] = self::$installed; 355 | } 356 | 357 | return $installed; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /vendor/composer/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) Nils Adermann, Jordi Boggiano 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /vendor/composer/autoload_classmap.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | ); 11 | -------------------------------------------------------------------------------- /vendor/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | array($vendorDir . '/imangazaliev/didom/src/DiDom'), 10 | ); 11 | -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | register(true); 35 | 36 | return $loader; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'DiDom\\' => 6, 13 | ), 14 | ); 15 | 16 | public static $prefixDirsPsr4 = array ( 17 | 'DiDom\\' => 18 | array ( 19 | 0 => __DIR__ . '/..' . '/imangazaliev/didom/src/DiDom', 20 | ), 21 | ); 22 | 23 | public static $classMap = array ( 24 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 25 | ); 26 | 27 | public static function getInitializer(ClassLoader $loader) 28 | { 29 | return \Closure::bind(function () use ($loader) { 30 | $loader->prefixLengthsPsr4 = ComposerStaticInit8a23e2ae4d3f41dc1f0d10df35e696c7::$prefixLengthsPsr4; 31 | $loader->prefixDirsPsr4 = ComposerStaticInit8a23e2ae4d3f41dc1f0d10df35e696c7::$prefixDirsPsr4; 32 | $loader->classMap = ComposerStaticInit8a23e2ae4d3f41dc1f0d10df35e696c7::$classMap; 33 | 34 | }, null, ClassLoader::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { 4 | "name": "imangazaliev/didom", 5 | "version": "2.0.1", 6 | "version_normalized": "2.0.1.0", 7 | "source": { 8 | "type": "git", 9 | "url": "https://github.com/Imangazaliev/DiDOM.git", 10 | "reference": "50fa6595d14f22c0c984efed5c818485cf548136" 11 | }, 12 | "dist": { 13 | "type": "zip", 14 | "url": "https://api.github.com/repos/Imangazaliev/DiDOM/zipball/50fa6595d14f22c0c984efed5c818485cf548136", 15 | "reference": "50fa6595d14f22c0c984efed5c818485cf548136", 16 | "shasum": "" 17 | }, 18 | "require": { 19 | "ext-dom": "*", 20 | "ext-iconv": "*", 21 | "php": ">=7.2" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^8.5" 25 | }, 26 | "time": "2023-03-05T03:23:48+00:00", 27 | "type": "library", 28 | "installation-source": "dist", 29 | "autoload": { 30 | "psr-4": { 31 | "DiDom\\": "src/DiDom/" 32 | } 33 | }, 34 | "notification-url": "https://packagist.org/downloads/", 35 | "license": [ 36 | "MIT" 37 | ], 38 | "authors": [ 39 | { 40 | "name": "Imangazaliev Muhammad", 41 | "email": "imangazalievm@gmail.com" 42 | } 43 | ], 44 | "description": "Simple and fast HTML parser", 45 | "homepage": "https://github.com/Imangazaliev/DiDOM", 46 | "keywords": [ 47 | "didom", 48 | "html", 49 | "parser", 50 | "xml" 51 | ], 52 | "support": { 53 | "issues": "https://github.com/Imangazaliev/DiDOM/issues", 54 | "source": "https://github.com/Imangazaliev/DiDOM/tree/2.0.1" 55 | }, 56 | "install-path": "../imangazaliev/didom" 57 | } 58 | ], 59 | "dev": true, 60 | "dev-package-names": [] 61 | } 62 | -------------------------------------------------------------------------------- /vendor/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => '__root__', 4 | 'pretty_version' => 'dev-develop', 5 | 'version' => 'dev-develop', 6 | 'reference' => '917e8b2d0b813e0b921ef084eaf4bf349ce8ef76', 7 | 'type' => 'library', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => true, 11 | ), 12 | 'versions' => array( 13 | '__root__' => array( 14 | 'pretty_version' => 'dev-develop', 15 | 'version' => 'dev-develop', 16 | 'reference' => '917e8b2d0b813e0b921ef084eaf4bf349ce8ef76', 17 | 'type' => 'library', 18 | 'install_path' => __DIR__ . '/../../', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | 'imangazaliev/didom' => array( 23 | 'pretty_version' => '2.0.1', 24 | 'version' => '2.0.1.0', 25 | 'reference' => '50fa6595d14f22c0c984efed5c818485cf548136', 26 | 'type' => 'library', 27 | 'install_path' => __DIR__ . '/../imangazaliev/didom', 28 | 'aliases' => array(), 29 | 'dev_requirement' => false, 30 | ), 31 | ), 32 | ); 33 | -------------------------------------------------------------------------------- /vendor/composer/platform_check.php: -------------------------------------------------------------------------------- 1 | = 70200)) { 8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.'; 9 | } 10 | 11 | if ($issues) { 12 | if (!headers_sent()) { 13 | header('HTTP/1.1 500 Internal Server Error'); 14 | } 15 | if (!ini_get('display_errors')) { 16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { 17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); 18 | } elseif (!headers_sent()) { 19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; 20 | } 21 | } 22 | trigger_error( 23 | 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 | E_USER_ERROR 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0 2 | 3 | ### Breaking changes 4 | 5 | - Minimum PHP version bumped to 7.2 6 | - Remove `__invoke` method from `Document`, `Element` and `DocumentFragment` that was deprecated early 7 | - Remove magic property `Element::$tag`. Use `tagName()` method instead 8 | - Rename `Element::getDocument()` to `ownerDocument()` 9 | 10 | ### What's new 11 | 12 | - Add `Node::setInnerXml()` method (i. e. for `Element` and `DocumentFragment` too) 13 | 14 | ## 1.18 15 | 16 | - Fix a bug when a call of Element::previousSibling() with selector returns a previous sibling when there is not matching element 17 | 18 | ## 1.17 19 | 20 | - Add support of multiple pseudoclasses (#125) 21 | 22 | ## 1.16.4 23 | 24 | - Handle nested pseudo-classes with expression correctly 25 | 26 | ## 1.16.3 27 | 28 | - Fix parsing of a style property in "style" attribute when the value contains a colon 29 | 30 | ## 1.16.1 31 | 32 | - Fix deprecation notice in PHP 8 for `libxml_disable_entity_loader` 33 | 34 | ## 1.16 35 | 36 | - Add `Node::insertSiblingBefore()` and `Node::insertSiblingAfter()` methods for inserting sibling nodes 37 | 38 | ## 1.15 39 | 40 | - Add support of document fragments 41 | 42 | ## 1.14.1 43 | 44 | - Fix an exception when selecting comment element with XPath 45 | - Add support of `DOMCdataSection` nodes 46 | - Add methods `createTextNode()`, `createComment()`, `createCdataSection()` to the Document class 47 | 48 | ## 1.14 49 | 50 | - Add `Element::innerXml()` method 51 | 52 | ## 1.13 53 | 54 | - Add `Element::outerHtml()` method 55 | - Add `Element::prependChild()` method 56 | - Add `Element::insertBefore()` and `Element::insertAfter()` methods 57 | - Add `Element::style()` method for more convenient inline styles manipulation 58 | - Add `Element::classes()` method for more convenient class manipulation 59 | 60 | ## 1.12 61 | 62 | - Many fixes and improvements 63 | 64 | ## 1.11.1 65 | 66 | - Fix bug with unregistered PHP functions in XPath in `Document::has()` and `Document::count()` methods 67 | 68 | ## 1.11 69 | 70 | - Add `Element::isElementNode()` method 71 | - Add ability to retrieve only specific attributes in `Element::attributes()` method 72 | - Add `Element::removeAllAttributes()` method 73 | - Add ability to specify selector and node type in `Element::previousSibling()` and `Element::nextSibling()` methods 74 | - Add `Element::previousSiblings()` and `Element::nextSiblings()` methods 75 | - Many minor fixes and improvements 76 | 77 | ## 1.10.6 78 | 79 | - Fix bug with XML document loading 80 | 81 | ## 1.10.5 82 | 83 | - Fix issue #85 84 | 85 | ## 1.10.4 86 | 87 | - Use `mb_convert_encoding()` in the Encoder if it is available 88 | 89 | ## 1.10.3 90 | 91 | - Add `Element::removeChild()` and `Element::removeChildren()` methods 92 | - Fix bug in `Element::matches()` method 93 | - `Element::matches()` method now returns false if node is not `DOMElement` 94 | - Add `Element::hasChildren()` method 95 | 96 | ## 1.10.2 97 | 98 | - Fix bug in setInnerHtml: can't rewrite existing content 99 | - Throw `InvalidSelectorException` instead of `InvalidArgumentException` when selector is empty 100 | 101 | ## 1.10.1 102 | 103 | - Fix attributes `ends-with` XPath 104 | - Method `Element::matches()` now can check children nodes 105 | 106 | ## 1.10 107 | 108 | - Fix HTML saving mechanism 109 | - Throw `InvalidSelectorException` instead of `RuntimeException` in Query class 110 | 111 | ## 1.9.1 112 | 113 | - Add ability to search in owner document using current node as context 114 | - Bugs fixed 115 | 116 | ## 1.9.0 117 | 118 | - Methods `Document::appendChild()` and `Element::appendChild()` now return appended node(s) 119 | - Add ability to search elements in context 120 | 121 | ## 1.8.8 122 | 123 | - Bugs fixed 124 | 125 | ## 1.8.7 126 | 127 | - Add `Element::getLineNo()` method 128 | 129 | ## 1.8.6 130 | 131 | - Fix issue #55 132 | 133 | ## 1.8.5 134 | 135 | - Add support of `DOMComment` 136 | 137 | ## 1.8.4 138 | 139 | - Add ability to create an element by selector 140 | - Add closest method 141 | 142 | ## 1.8.3 143 | 144 | - Add method `Element::isTextNode()` 145 | - Many minor fixes 146 | 147 | ## 1.8.2 148 | 149 | - Add ability to check that element matches selector 150 | - Add ability counting nodes by selector 151 | - Many minor fixes 152 | 153 | ## 1.8.1 154 | 155 | - Small fix 156 | 157 | ## 1.8 158 | 159 | - Bug fixes 160 | - Add support of ~ selector 161 | - Add ability to direct search by CSS selector 162 | - Add setInnerHtml method 163 | - Add attributes method 164 | 165 | ## 1.7.4 166 | 167 | - Add support of text nodes 168 | 169 | ## 1.7.3 170 | 171 | - Bug fix 172 | 173 | ## 1.7.2 174 | 175 | - Fixed behavior of nth-child pseudo class 176 | - Add nth-of-type pseudo class 177 | 178 | ## 1.7.1 179 | 180 | - Add pseudo class has and more attribute options 181 | 182 | ## 1.7.0 183 | 184 | - Bug fixes 185 | - Add methods `previousSibling`, `nextSibling`, `child`, `firstChild`, `lastChild`, `children`, `getDocument` to the Element 186 | - Changed behavior of parent method. Now it returns parent node instead of owner document 187 | 188 | ## 1.6.8 189 | 190 | - Bug fix 191 | 192 | ## 1.6.5 193 | 194 | - Added ability to get an element attribute by CSS selector 195 | 196 | ## 1.6.4 197 | 198 | - Added handling of `DOMText` and `DOMAttr` in `Document::find()` 199 | 200 | ## 1.6.3 201 | 202 | - Added ability to get inner HTML 203 | 204 | ## 1.6.2 205 | 206 | - Added the ability to pass options when load HTML or XML 207 | 208 | ## 1.6.1 209 | 210 | - Added the ability to pass an array of nodes to appendChild 211 | - Added the ability to pass options when converting to HTML or XML 212 | - Added the ability to add child elements to the element 213 | 214 | ## 1.6 215 | 216 | - Added support for XML 217 | - Added the ability to search element by part of attribute name or value 218 | - Added support for pseudo-class "contains" 219 | - Added the ability to clone a node 220 | 221 | ## 1.5.1 222 | 223 | - Added ability to remove and replace nodes 224 | - Added ability to specify encoding when converting the element into the document 225 | 226 | ## 1.5 227 | 228 | - Fixed problem with incorrect encoding 229 | - Added ability to set the value of the element 230 | - Added ability to specify encoding when creating document 231 | 232 | ## 1.4 233 | 234 | - Added the ability to specify the return type element (`DiDom\Element` or `DOMElement`) 235 | 236 | ## 1.3.2 237 | 238 | - Bug fixed 239 | 240 | ## 1.3.1 241 | 242 | - Bugs fixed 243 | - Added the ability to pass element attributes in the constructor 244 | 245 | ## 1.3 246 | 247 | - Bugs fixed 248 | 249 | ## 1.2 250 | 251 | - Bugs fixed 252 | - Added the ability to compare Element\Document 253 | - Added the ability to format HTML code of the document when outputting 254 | 255 | ## 1.1 256 | 257 | - Added cache control 258 | - Converter from CSS to XPath replaced by faster 259 | 260 | ## 1.0 261 | 262 | - First release -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Muhammad Imangazaliev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/README-RU.md: -------------------------------------------------------------------------------- 1 | # DiDOM 2 | 3 | [![Build Status](https://travis-ci.org/Imangazaliev/DiDOM.svg?branch=master)](https://travis-ci.org/Imangazaliev/DiDOM) 4 | [![Total Downloads](https://poser.pugx.org/imangazaliev/didom/downloads)](https://packagist.org/packages/imangazaliev/didom) 5 | [![Latest Stable Version](https://poser.pugx.org/imangazaliev/didom/v/stable)](https://packagist.org/packages/imangazaliev/didom) 6 | [![License](https://poser.pugx.org/imangazaliev/didom/license)](https://packagist.org/packages/imangazaliev/didom) 7 | 8 | DiDOM - простая и быстрая библиотека для парсинга HTML. 9 | 10 | - [English version](README.md) 11 | - [Документация для версии 1.x](https://github.com/Imangazaliev/DiDOM/blob/98d411741d598b0b74bb38e215d99c1cdb0d532d/README-RU.md). Чтобы обновится с версии 1.x, пожалуйста просмотрите [историю изменений](CHANGELOG.md). 12 | 13 | ## Содержание 14 | 15 | - [Установка](#Установка) 16 | - [Быстрый старт](#Быстрый-старт) 17 | - [Создание нового документа](#Создание-нового-документа) 18 | - [Поиск элементов](#Поиск-элементов) 19 | - [Проверка наличия элемента](#Проверка-наличия-элемента) 20 | - [Подсчет количества элементов](#Подсчет-количества-элементов) 21 | - [Поиск в элементе](#Поиск-в-элементе) 22 | - [Поддерживамые селекторы](#Поддерживамые-селекторы) 23 | - [Изменение содержимого](#Изменение-содержимого) 24 | - [Вывод содержимого](#Вывод-содержимого) 25 | - [Работа с элементами](#Работа-с-элементами) 26 | - [Создание нового элемента](#Создание-нового-элемента) 27 | - [Получение названия элемента](#Получение-названия-элемента) 28 | - [Получение родительского элемента](#Получение-родительского-элемента) 29 | - [Получение соседних элементов](#Получение-соседних-элементов) 30 | - [Получение дочерних элементов](#Получение-соседних-элементов) 31 | - [Получение документа](#Получение-документа) 32 | - [Работа с атрибутами элемента](#Работа-с-атрибутами-элемента) 33 | - [Сравнение элементов](#Сравнение-элементов) 34 | - [Добавление дочерних элементов](#Добавление-дочерних-элементов) 35 | - [Замена элемента](#Замена-элемента) 36 | - [Удаление элемента](#Удаление-элемента) 37 | - [Работа с кэшем](#Работа-с-кэшем) 38 | - [Прочее](#Прочее) 39 | - [Сравнение с другими парсерами](#Сравнение-с-другими-парсерами) 40 | 41 | ## Установка 42 | 43 | Для установки DiDOM выполните команду: 44 | 45 | composer require imangazaliev/didom 46 | 47 | ## Быстрый старт 48 | 49 | ```php 50 | use DiDom\Document; 51 | 52 | $document = new Document('http://www.news.com/', true); 53 | 54 | $posts = $document->find('.post'); 55 | 56 | foreach($posts as $post) { 57 | echo $post->text(), "\n"; 58 | } 59 | ``` 60 | 61 | ## Создание нового документа 62 | 63 | DiDom позволяет загрузить HTML несколькими способами: 64 | 65 | ##### Через конструктор 66 | 67 | ```php 68 | // в первом параметре передается строка с HTML 69 | $document = new Document($html); 70 | 71 | // путь к файлу 72 | $document = new Document('page.html', true); 73 | 74 | // или URL 75 | $document = new Document('http://www.example.com/', true); 76 | 77 | // также можно создать документ из DOMDocument 78 | $domDocument = new DOMDocument(); 79 | $document = new Document($domDocument); 80 | ``` 81 | 82 | Сигнатура: 83 | 84 | ```php 85 | __construct($string = null, $isFile = false, $encoding = 'UTF-8', $type = Document::TYPE_HTML) 86 | ``` 87 | 88 | `$isFile` - указывает, что загружается файл. По умолчанию - `false`. 89 | 90 | `$encoding` - кодировка документа. По умолчанию - UTF-8. 91 | 92 | `$type` - тип документа (HTML - `Document::TYPE_HTML`, XML - `Document::TYPE_XML`). По умолчанию - `Document::TYPE_HTML`. 93 | 94 | ##### Через отдельные методы 95 | 96 | ```php 97 | $document = new Document(); 98 | 99 | $document->loadHtml($html); 100 | 101 | $document->loadHtmlFile('page.html'); 102 | 103 | $document->loadHtmlFile('http://www.example.com/'); 104 | ``` 105 | 106 | Для загрузки XML есть соответствующие методы `loadXml` и `loadXmlFile`. 107 | 108 | При загрузке документа через эти методы, парсеру можно передать дополнительные [опции](http://php.net/manual/ru/libxml.constants.php): 109 | 110 | ```php 111 | $document->loadHtml($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 112 | $document->loadHtmlFile($url, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 113 | 114 | $document->loadXml($xml, LIBXML_PARSEHUGE); 115 | $document->loadXmlFile($url, LIBXML_PARSEHUGE); 116 | ``` 117 | 118 | ## Поиск элементов 119 | 120 | В качестве выражения для поиска можно передать CSS-селектор или XPath. Для этого в первом параметре нужно передать само выражение, а во втором - его тип (по умолчанию - `Query::TYPE_CSS`): 121 | 122 | ##### Через метод `find()`: 123 | 124 | ```php 125 | use DiDom\Document; 126 | use DiDom\Query; 127 | 128 | ... 129 | 130 | // CSS-селектор 131 | $posts = $document->find('.post'); 132 | 133 | // эквивалентно 134 | $posts = $document->find('.post', Query::TYPE_CSS); 135 | 136 | // XPath-выражение 137 | $posts = $document->find("//div[contains(@class, 'post')]", Query::TYPE_XPATH); 138 | ``` 139 | 140 | Метод вернет массив с элементами (экземпляры класса `DiDom\Element`) или пустой массив, если не найден ни один элемент, соответствующий выражению. 141 | 142 | При желании можно получить массив узлов без преобразования в Element или текст (`DOMElement`/`DOMText`/`DOMComment`/`DOMAttr`, в зависимости от выражения), для этого необходимо передать в качестве третьего параметра `false`. 143 | 144 | ##### Через метод `first()`: 145 | 146 | Возвращает первый найденный элемент или `null`, если не найдено ни одного элемента. 147 | 148 | Принимает те же параметры, что и метод `find()`. 149 | 150 | ##### Через магический метод `__invoke()`: 151 | 152 | ```php 153 | $posts = $document('.post'); 154 | ``` 155 | 156 | Принимает те же параметры, что и метод `find()`. 157 | 158 | **Внимание:** использование данного метода нежелательно, т.к. в будущем он может быть удален. 159 | 160 | ##### Через метод `xpath()`: 161 | 162 | ```php 163 | $posts = $document->xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' post ')]"); 164 | ``` 165 | 166 | ## Проверка наличия элемента 167 | 168 | Проверить наличие элемента можно с помощью метода `has()`: 169 | 170 | ```php 171 | if ($document->has('.post')) { 172 | // код 173 | } 174 | ``` 175 | 176 | Если нужно проверить наличие элемента, а затем получить его, то можно сделать так: 177 | 178 | ```php 179 | if ($document->has('.post')) { 180 | $elements = $document->find('.post'); 181 | 182 | // код 183 | } 184 | ``` 185 | 186 | но быстрее так: 187 | 188 | ```php 189 | $elements = $document->find('.post'); 190 | 191 | if (count($elements) > 0) { 192 | // код 193 | } 194 | ``` 195 | 196 | т.к. в первом случае выполняется два запроса. 197 | 198 | ## Подсчет количества элементов 199 | 200 | Метод `count()` позволяет подсчитать количество дочерних элементов, соотвествующих селектору: 201 | 202 | ```php 203 | // выведет количество ссылок в документе 204 | echo $document->count('a'); 205 | ``` 206 | 207 | ```php 208 | // выведет количество пунктов в списке 209 | echo $document->first('ul')->count('> li'); 210 | ``` 211 | 212 | ## Поиск в элементе 213 | 214 | Методы `find()`, `first()`, `xpath()`, `has()`, `count()` доступны также и для элемента. 215 | 216 | Пример: 217 | 218 | ```php 219 | echo $document->find('nav')[0]->first('ul.menu')->xpath('//li')[0]->text(); 220 | ``` 221 | 222 | #### Метод `findInDocument()` 223 | 224 | При изменении, замене или удалении элемента, найденного в другом элементе, документ не будет изменен. Данное поведение связано с тем, что в методе `find()` класса `Element` (а, соответственно, и в методах `first()` и `xpath`) создается новый документ, в котором и производится поиск. 225 | 226 | Для поиска элементов в исходном документе необходимо использовать методы `findInDocument()` и `firstInDocument()`: 227 | 228 | ```php 229 | // ничего не выйдет 230 | $document->first('head')->first('title')->remove(); 231 | 232 | // а вот так да 233 | $document->first('head')->firstInDocument('title')->remove(); 234 | ``` 235 | 236 | **Внимание:** методы `findInDocument()` и `firstInDocument()` работают только для элементов, которые принадлежат какому-либо документу, либо созданых через `new Element(...)`. Если элемент не принадлежит к какому-либо документу, будет выброшено исключение `LogicException`; 237 | 238 | ## Поддерживамые селекторы 239 | 240 | DiDom поддерживает поиск по: 241 | 242 | - тэгу 243 | - классу, идентификатору, имени и значению атрибута 244 | - псевдоклассам: 245 | - first-, last-, nth-child 246 | - empty и not-empty 247 | - contains 248 | - has 249 | 250 | ```php 251 | // все ссылки 252 | $document->find('a'); 253 | 254 | // любой элемент с id = "foo" и классом "bar" 255 | $document->find('#foo.bar'); 256 | 257 | // любой элемент, у которого есть атрибут "name" 258 | $document->find('[name]'); 259 | 260 | // эквивалентно 261 | $document->find('*[name]'); 262 | 263 | // поле ввода с именем "foo" 264 | $document->find('input[name=foo]'); 265 | $document->find('input[name=\'foo\']'); 266 | $document->find('input[name="foo"]'); 267 | 268 | // поле ввода с именем "foo" и значением "bar" 269 | $document->find('input[name="foo"][value="bar"]'); 270 | 271 | // поле ввода, название которого НЕ равно "foo" 272 | $document->find('input[name!="foo"]'); 273 | 274 | // любой элемент, у которого есть атрибут, 275 | // начинающийся с "data-" и равный "foo" 276 | $document->find('*[^data-=foo]'); 277 | 278 | // все ссылки, у которых адрес начинается с https 279 | $document->find('a[href^=https]'); 280 | 281 | // все изображения с расширением png 282 | $document->find('img[src$=png]'); 283 | 284 | // все ссылки, содержащие в своем адресе строку "example.com" 285 | $document->find('a[href*=example.com]'); 286 | 287 | // все ссылки, содержащие в атрибуте data-foo значение bar отделенное пробелом 288 | $document->find('a[data-foo~=bar]'); 289 | 290 | // текст всех ссылок с классом "foo" (массив строк) 291 | $document->find('a.foo::text'); 292 | 293 | // эквивалентно 294 | $document->find('a.foo::text()'); 295 | 296 | // адрес и текст подсказки всех полей с классом "bar" 297 | $document->find('a.bar::attr(href|title)'); 298 | 299 | // все ссылки, которые являются прямыми потомками текущего элемента 300 | $element->find('> a'); 301 | ``` 302 | 303 | ## Изменение содержимого 304 | 305 | ### Изменение HTML 306 | 307 | ```php 308 | $element->setInnerHtml('Foo'); 309 | ``` 310 | 311 | ### Изменение XML 312 | 313 | ```php 314 | $element->setInnerXml(' Foo BarHello world! 316 | ]]>'); 317 | ``` 318 | 319 | ### Изменение значения (как простой текст) 320 | 321 | ```php 322 | $element->setValue('Foo'); 323 | // будет закодирован в HTML-сущность как при вызове htmlentities() 324 | $element->setValue('Foo'); 325 | ``` 326 | 327 | ## Вывод содержимого 328 | 329 | ### Получение HTML 330 | 331 | ##### Через метод `html()`: 332 | 333 | ```php 334 | // HTML-код документа 335 | echo $document->html(); 336 | 337 | // HTML-код элемента 338 | echo $document->first('.post')->html(); 339 | ``` 340 | 341 | ##### Приведение к строке: 342 | 343 | ```php 344 | // HTML-код документа 345 | $html = (string) $document; 346 | 347 | // HTML-код элемента 348 | $html = (string) $document->first('.post'); 349 | ``` 350 | 351 | **Внимание:** использование данного способа нежелательно, т.к. в будущем он может быть удален. 352 | 353 | ##### Форматирование HTML при выводе 354 | 355 | ```php 356 | echo $document->format()->html(); 357 | ``` 358 | 359 | Метод `format()` отсутствует у элемента, поэтому, если нужно получить отформатированный HTML-код элемента, необходимо сначала преобразовать его в документ: 360 | 361 | ```php 362 | $html = $element->toDocument()->format()->html(); 363 | ``` 364 | 365 | #### Внутренний HTML 366 | 367 | ```php 368 | $innerHtml = $element->innerHtml(); 369 | ``` 370 | 371 | Метод `innerHtml()` отсутствует у документа, поэтому, если нужно получить внутренний HTML-код документа, необходимо сначала преобразовать его в элемент: 372 | 373 | ```php 374 | $innerHtml = $document->toElement()->innerHtml(); 375 | ``` 376 | 377 | ### Получение XML 378 | 379 | ```php 380 | // XML-код документа 381 | echo $document->xml(); 382 | 383 | // XML-код элемента 384 | echo $document->first('book')->xml(); 385 | ``` 386 | 387 | ### Получение содержимого 388 | 389 | Возвращает текстовое содержимое узла и его потомков: 390 | 391 | ```php 392 | echo $element->text(); 393 | ``` 394 | 395 | ## Создание нового элемента 396 | 397 | ### Создание экземпляра класса 398 | 399 | ```php 400 | use DiDom\Element; 401 | 402 | $element = new Element('span', 'Hello'); 403 | 404 | // выведет "Hello" 405 | echo $element->html(); 406 | ``` 407 | 408 | Первым параметром передается название элемента, вторым - его значение (необязательно), третьим - атрибуты элемента (необязательно). 409 | 410 | Пример создания элемента с атрибутами: 411 | 412 | ```php 413 | $attributes = ['name' => 'description', 'placeholder' => 'Enter description of item']; 414 | 415 | $element = new Element('textarea', 'Text', $attributes); 416 | ``` 417 | 418 | Элемент можно создать и из экземпляра класса `DOMElement`: 419 | 420 | ```php 421 | use DiDom\Element; 422 | use DOMElement; 423 | 424 | $domElement = new DOMElement('span', 'Hello'); 425 | $element = new Element($domElement); 426 | ``` 427 | 428 | #### Изменение элемента, созданного из `DOMElement` 429 | 430 | Экземпляры класса `DOMElement`, созданные через конструктор (`new DOMElement(...)`), являются неизменяемыми, поэтому и элементы (экземпляры класса `DiDom\Element`), созданные из таких объектов, так же являются неизменяемыми. 431 | 432 | Пример: 433 | 434 | ```php 435 | $element = new Element('span', 'Hello'); 436 | 437 | // добавит атрибут "id" со значением "greeting" 438 | $element->attr('id', 'greeting'); 439 | 440 | $domElement = new DOMElement('span', 'Hello'); 441 | $element = new Element($domElement); 442 | 443 | // будет выброшено исключение 444 | // DOMException with message 'No Modification Allowed Error' 445 | $element->attr('id', 'greeting'); 446 | ``` 447 | 448 | ### С помощью метода `Document::createElement()` 449 | 450 | ```php 451 | $document = new Document($html); 452 | 453 | $element = $document->createElement('span', 'Hello'); 454 | ``` 455 | 456 | ### С помощью CSS-селектора 457 | 458 | Первый параметр - селектор, второй - значение, третий - массив с атрибутами. 459 | 460 | Атрибуты элемента могут быть указаны как в селекторе, так и переданы отдельно в третьем параметре. 461 | 462 | Если название атрибута в массиве совпадает с названием атрибута из селектора, будет использовано значение, указанное в селекторе. 463 | 464 | ```php 465 | $document = new Document($html); 466 | 467 | $element = $document->createElementBySelector('div.block', 'Foo', [ 468 | 'id' => '#content', 469 | 'class' => '.container', 470 | ]); 471 | ``` 472 | 473 | Можно так же использовать статический метод `createBySelector` класса `Element`: 474 | 475 | ```php 476 | $element = Element::createBySelector('div.block', 'Foo', [ 477 | 'id' => '#content', 478 | 'class' => '.container', 479 | ]); 480 | ``` 481 | 482 | ## Получение названия элемента 483 | 484 | ```php 485 | $element->tagName(); 486 | ``` 487 | 488 | ## Получение родительского элемента 489 | 490 | ```php 491 | $element->parent(); 492 | ``` 493 | 494 | Так же можно получить родительский элемент, соответствующий селектору: 495 | 496 | ```php 497 | $element->closest('.foo'); 498 | ``` 499 | 500 | Вернет родительский элемент, у которого есть класс `foo`. Если подходящий элемент не найден, метод вернет `null`. 501 | 502 | ## Получение соседних элементов 503 | 504 | Первый аргумент - CSS-селектор, второй - тип узла (`DOMElement`, `DOMText` или `DOMComment`). 505 | 506 | Если оба аргумента опущены, будет осуществлен поиск узлов любого типа. 507 | 508 | Если селектор указан, а тип узла нет, будет использован тип `DOMElement`. 509 | 510 | **Внимание:** Селектор можно использовать только с типом `DOMElement`. 511 | 512 | ```php 513 | // предыдущий элемент 514 | $item->previousSibling(); 515 | 516 | // предыдущий элемент, соответствующий селектору 517 | $item->previousSibling('span'); 518 | 519 | // предыдущий элемент типа DOMElement 520 | $item->previousSibling(null, 'DOMElement'); 521 | 522 | // предыдущий элемент типа DOMComment 523 | $item->previousSibling(null, 'DOMComment'); 524 | ``` 525 | 526 | ```php 527 | // все предыдущие элементы 528 | $item->previousSiblings(); 529 | 530 | // все предыдущие элементы, соответствующие селектору 531 | $item->previousSiblings('span'); 532 | 533 | // все предыдущие элементы типа DOMElement 534 | $item->previousSiblings(null, 'DOMElement'); 535 | 536 | // все предыдущие элементы типа DOMComment 537 | $item->previousSiblings(null, 'DOMComment'); 538 | ``` 539 | 540 | ```php 541 | // следующий элемент 542 | $item->nextSibling(); 543 | 544 | // следующий элемент, соответствующий селектору 545 | $item->nextSibling('span'); 546 | 547 | // следующий элемент типа DOMElement 548 | $item->nextSibling(null, 'DOMElement'); 549 | 550 | // следующий элемент типа DOMComment 551 | $item->nextSibling(null, 'DOMComment'); 552 | ``` 553 | 554 | ```php 555 | // все последующие элементы 556 | $item->nextSiblings(); 557 | 558 | // все последующие элементы, соответствующие селектору 559 | $item->nextSiblings('span'); 560 | 561 | // все последующие элементы типа DOMElement 562 | $item->nextSiblings(null, 'DOMElement'); 563 | 564 | // все последующие элементы типа DOMComment 565 | $item->nextSiblings(null, 'DOMComment'); 566 | ``` 567 | 568 | ## Получение дочерних элементов 569 | 570 | ```php 571 | $html = '

FooBar
'; 572 | 573 | $document = new Document($html); 574 | 575 | $div = $document->first('div'); 576 | 577 | // элемент (DOMElement) 578 | // string(3) "Bar" 579 | var_dump($div->child(1)->text()); 580 | 581 | // текстовый узел (DOMText) 582 | // string(3) "Foo" 583 | var_dump($div->firstChild()->text()); 584 | 585 | // комментарий (DOMComment) 586 | // string(3) "Baz" 587 | var_dump($div->lastChild()->text()); 588 | 589 | // array(3) { ... } 590 | var_dump($div->children()); 591 | ``` 592 | 593 | ## Получение документа 594 | 595 | ```php 596 | $document = new Document($html); 597 | 598 | $element = $document->first('input[name=email]'); 599 | 600 | $document2 = $element->ownerDocument(); 601 | 602 | // bool(true) 603 | var_dump($document->is($document2)); 604 | ``` 605 | 606 | ## Работа с атрибутами элемента 607 | 608 | #### Создание/изменение атрибута 609 | 610 | ##### Через метод `setAttribute`: 611 | ```php 612 | $element->setAttribute('name', 'username'); 613 | ``` 614 | 615 | ##### Через метод `attr`: 616 | ```php 617 | $element->attr('name', 'username'); 618 | ``` 619 | 620 | ##### Через магический метод `__set`: 621 | ```php 622 | $element->name = 'username'; 623 | ``` 624 | 625 | #### Получение значения атрибута 626 | 627 | ##### Через метод `getAttribute`: 628 | ```php 629 | $username = $element->getAttribute('value'); 630 | ``` 631 | 632 | ##### Через метод `attr`: 633 | ```php 634 | $username = $element->attr('value'); 635 | ``` 636 | 637 | ##### Через магический метод `__get`: 638 | ```php 639 | $username = $element->name; 640 | ``` 641 | 642 | Если атрибут не найден, вернет `null`. 643 | 644 | #### Проверка наличия атрибута 645 | 646 | ##### Через метод `hasAttribute`: 647 | ```php 648 | if ($element->hasAttribute('name')) { 649 | // код 650 | } 651 | ``` 652 | 653 | ##### Через магический метод `__isset`: 654 | ```php 655 | if (isset($element->name)) { 656 | // код 657 | } 658 | ``` 659 | 660 | #### Удаление атрибута: 661 | 662 | ##### Через метод `removeAttribute`: 663 | ```php 664 | $element->removeAttribute('name'); 665 | ``` 666 | 667 | ##### Через магический метод `__unset`: 668 | ```php 669 | unset($element->name); 670 | ``` 671 | 672 | #### Получение всех атрибутов: 673 | 674 | ```php 675 | var_dump($element->attributes()); 676 | ``` 677 | 678 | #### Получение определенных атрибутов: 679 | 680 | ```php 681 | var_dump($element->attributes(['name', 'type'])); 682 | ``` 683 | 684 | #### Удаление всех атрибутов: 685 | 686 | ```php 687 | $element->removeAllAttributes(); 688 | ``` 689 | 690 | #### Удаление всех атрибутов, за исключением указанных: 691 | 692 | ```php 693 | $element->removeAllAttributes(['name', 'type']); 694 | ``` 695 | 696 | ## Сравнение элементов 697 | 698 | ```php 699 | $element = new Element('span', 'hello'); 700 | $element2 = new Element('span', 'hello'); 701 | 702 | // bool(true) 703 | var_dump($element->is($element)); 704 | 705 | // bool(false) 706 | var_dump($element->is($element2)); 707 | ``` 708 | 709 | ## Добавление дочерних элементов 710 | 711 | ```php 712 | $list = new Element('ul'); 713 | 714 | $item = new Element('li', 'Item 1'); 715 | 716 | $list->appendChild($item); 717 | 718 | $items = [ 719 | new Element('li', 'Item 2'), 720 | new Element('li', 'Item 3'), 721 | ]; 722 | 723 | $list->appendChild($items); 724 | ``` 725 | 726 | ## Замена элемента 727 | 728 | ```php 729 | $title = new Element('title', 'foo'); 730 | 731 | $document->first('title')->replace($title); 732 | ``` 733 | 734 | **Внимание:** заменить можно только те элементы, которые были найдены непосредственно в документе: 735 | 736 | ```php 737 | // ничего не выйдет 738 | $document->first('head')->first('title')->replace($title); 739 | 740 | // а вот так да 741 | $document->first('head title')->replace($title); 742 | ``` 743 | 744 | Подробнее об этом в разделе [Поиск в элементе](#Поиск-в-элементе). 745 | 746 | ## Удаление элемента 747 | 748 | ```php 749 | $document->first('title')->remove(); 750 | ``` 751 | 752 | **Внимание:** удалить можно только те элементы, которые были найдены непосредственно в документе: 753 | 754 | ```php 755 | // ничего не выйдет 756 | $document->first('head')->first('title')->remove(); 757 | 758 | // а вот так да 759 | $document->first('head title')->remove(); 760 | ``` 761 | 762 | Подробнее об этом в разделе [Поиск в элементе](#Поиск-в-элементе). 763 | 764 | ## Работа с кэшем 765 | 766 | Кэш - массив XPath-выражений, полученных из CSS. 767 | 768 | #### Получение кэша 769 | 770 | ```php 771 | use DiDom\Query; 772 | 773 | ... 774 | 775 | $xpath = Query::compile('h2'); 776 | $compiled = Query::getCompiled(); 777 | 778 | // array('h2' => '//h2') 779 | var_dump($compiled); 780 | ``` 781 | 782 | #### Установка кэша 783 | 784 | ```php 785 | Query::setCompiled(['h2' => '//h2']); 786 | ``` 787 | 788 | ## Прочее 789 | 790 | #### `preserveWhiteSpace` 791 | 792 | По умолчанию сохранение пробелов между тегами отключено. 793 | 794 | Включать опцию `preserveWhiteSpace` следует до загрузки документа: 795 | 796 | ```php 797 | $document = new Document(); 798 | 799 | $document->preserveWhiteSpace(); 800 | 801 | $document->loadXml($xml); 802 | ``` 803 | 804 | #### `matches` 805 | 806 | Возвращает `true`, если элемент соответсвует селектору: 807 | 808 | ```php 809 | // вернет true, если элемент это div с идентификатором content 810 | $element->matches('div#content'); 811 | 812 | // строгое соответствие 813 | // вернет true, если элемент это div с идентификатором content и ничего более 814 | // если у элемента будут какие-либо другие атрибуты, метод вернет false 815 | $element->matches('div#content', true); 816 | ``` 817 | 818 | #### `isElementNode` 819 | 820 | Проверяет, является ли элемент узлом типа DOMElement: 821 | 822 | ```php 823 | $element->isElementNode(); 824 | ``` 825 | 826 | #### `isTextNode` 827 | 828 | Проверяет, является ли элемент текстовым узлом (DOMText): 829 | 830 | ```php 831 | $element->isTextNode(); 832 | ``` 833 | 834 | #### `isCommentNode` 835 | 836 | Проверяет, является ли элемент комментарием (DOMComment): 837 | 838 | ```php 839 | $element->isCommentNode(); 840 | ``` 841 | 842 | ## Сравнение с другими парсерами 843 | 844 | [Сравнение с другими парсерами](https://github.com/Imangazaliev/DiDOM/wiki/Сравнение-с-другими-парсерами-(1.6.3)) 845 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/README.md: -------------------------------------------------------------------------------- 1 | # DiDOM 2 | 3 | [![Build Status](https://travis-ci.com/Imangazaliev/DiDOM.svg?branch=master)](https://travis-ci.com/Imangazaliev/DiDOM) 4 | [![Total Downloads](https://poser.pugx.org/imangazaliev/didom/downloads)](https://packagist.org/packages/imangazaliev/didom) 5 | [![Latest Stable Version](https://poser.pugx.org/imangazaliev/didom/v/stable)](https://packagist.org/packages/imangazaliev/didom) 6 | [![License](https://poser.pugx.org/imangazaliev/didom/license)](https://packagist.org/packages/imangazaliev/didom) 7 | 8 | DiDOM - simple and fast HTML parser. 9 | 10 | - [README на русском](README-RU.md) 11 | - [DiDOM 1.x documentation](https://github.com/Imangazaliev/DiDOM/blob/98d411741d598b0b74bb38e215d99c1cdb0d532d/README.md). To upgrade from 1.x please checkout the [changelog](CHANGELOG.md). 12 | 13 | ## Contents 14 | 15 | - [Installation](#installation) 16 | - [Quick start](#quick-start) 17 | - [Creating new document](#creating-new-document) 18 | - [Search for elements](#search-for-elements) 19 | - [Verify if element exists](#verify-if-element-exists) 20 | - [Search in element](#search-in-element) 21 | - [Supported selectors](#supported-selectors) 22 | - [Changing content](#changing-content) 23 | - [Output](#output) 24 | - [Working with elements](#working-with-elements) 25 | - [Creating a new element](#creating-a-new-element) 26 | - [Getting the name of an element](#getting-the-name-of-an-element) 27 | - [Getting parent element](#getting-parent-element) 28 | - [Getting sibling elements](#getting-sibling-elements) 29 | - [Getting the child elements](#getting-the-child-elements) 30 | - [Getting document](#getting-document) 31 | - [Working with element attributes](#working-with-element-attributes) 32 | - [Comparing elements](#comparing-elements) 33 | - [Adding a child element](#adding-a-child-element) 34 | - [Replacing element](#replacing-element) 35 | - [Removing element](#removing-element) 36 | - [Working with cache](#working-with-cache) 37 | - [Miscellaneous](#miscellaneous) 38 | - [Comparison with other parsers](#comparison-with-other-parsers) 39 | 40 | ## Installation 41 | 42 | To install DiDOM run the command: 43 | 44 | composer require imangazaliev/didom 45 | 46 | ## Quick start 47 | 48 | ```php 49 | use DiDom\Document; 50 | 51 | $document = new Document('http://www.news.com/', true); 52 | 53 | $posts = $document->find('.post'); 54 | 55 | foreach($posts as $post) { 56 | echo $post->text(), "\n"; 57 | } 58 | ``` 59 | 60 | ## Creating new document 61 | 62 | DiDom allows to load HTML in several ways: 63 | 64 | ##### With constructor 65 | 66 | ```php 67 | // the first parameter is a string with HTML 68 | $document = new Document($html); 69 | 70 | // file path 71 | $document = new Document('page.html', true); 72 | 73 | // or URL 74 | $document = new Document('http://www.example.com/', true); 75 | ``` 76 | 77 | The second parameter specifies if you need to load file. Default is `false`. 78 | 79 | Signature: 80 | 81 | ```php 82 | __construct($string = null, $isFile = false, $encoding = 'UTF-8', $type = Document::TYPE_HTML) 83 | ``` 84 | 85 | `$string` - an HTML or XML string or a file path. 86 | 87 | `$isFile` - indicates that the first parameter is a path to a file. 88 | 89 | `$encoding` - the document encoding. 90 | 91 | `$type` - the document type (HTML - `Document::TYPE_HTML`, XML - `Document::TYPE_XML`). 92 | 93 | ##### With separate methods 94 | 95 | ```php 96 | $document = new Document(); 97 | 98 | $document->loadHtml($html); 99 | 100 | $document->loadHtmlFile('page.html'); 101 | 102 | $document->loadHtmlFile('http://www.example.com/'); 103 | ``` 104 | 105 | There are two methods available for loading XML: `loadXml` and `loadXmlFile`. 106 | 107 | These methods accept additional [options](http://php.net/manual/en/libxml.constants.php): 108 | 109 | ```php 110 | $document->loadHtml($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 111 | $document->loadHtmlFile($url, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 112 | 113 | $document->loadXml($xml, LIBXML_PARSEHUGE); 114 | $document->loadXmlFile($url, LIBXML_PARSEHUGE); 115 | ``` 116 | 117 | ## Search for elements 118 | 119 | DiDOM accepts CSS selector or XPath as an expression for search. You need to path expression as the first parameter, and specify its type in the second one (default type is `Query::TYPE_CSS`): 120 | 121 | ##### With method `find()`: 122 | 123 | ```php 124 | use DiDom\Document; 125 | use DiDom\Query; 126 | 127 | ... 128 | 129 | // CSS selector 130 | $posts = $document->find('.post'); 131 | 132 | // XPath 133 | $posts = $document->find("//div[contains(@class, 'post')]", Query::TYPE_XPATH); 134 | ``` 135 | 136 | If the elements that match a given expression are found, then method returns an array of instances of `DiDom\Element`, otherwise - an empty array. You could also get an array of `DOMElement` objects. To get this, pass `false` as the third parameter. 137 | 138 | ##### With magic method `__invoke()`: 139 | 140 | ```php 141 | $posts = $document('.post'); 142 | ``` 143 | 144 | **Warning:** using this method is undesirable because it may be removed in the future. 145 | 146 | ##### With method `xpath()`: 147 | 148 | ```php 149 | $posts = $document->xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' post ')]"); 150 | ``` 151 | 152 | You can do search inside an element: 153 | 154 | ```php 155 | echo $document->find('nav')[0]->first('ul.menu')->xpath('//li')[0]->text(); 156 | ``` 157 | 158 | ### Verify if element exists 159 | 160 | To verify if element exist use `has()` method: 161 | 162 | ```php 163 | if ($document->has('.post')) { 164 | // code 165 | } 166 | ``` 167 | 168 | If you need to check if element exist and then get it: 169 | 170 | ```php 171 | if ($document->has('.post')) { 172 | $elements = $document->find('.post'); 173 | // code 174 | } 175 | ``` 176 | 177 | but it would be faster like this: 178 | 179 | ```php 180 | if (count($elements = $document->find('.post')) > 0) { 181 | // code 182 | } 183 | ``` 184 | 185 | because in the first case it makes two queries. 186 | 187 | ## Search in element 188 | 189 | Methods `find()`, `first()`, `xpath()`, `has()`, `count()` are available in Element too. 190 | 191 | Example: 192 | 193 | ```php 194 | echo $document->find('nav')[0]->first('ul.menu')->xpath('//li')[0]->text(); 195 | ``` 196 | 197 | #### Method `findInDocument()` 198 | 199 | If you change, replace, or remove an element that was found in another element, the document will not be changed. This happens because method `find()` of `Element` class (a, respectively, the `first ()` and `xpath` methods) creates a new document to search. 200 | 201 | To search for elements in the source document, you must use the methods `findInDocument()` and `firstInDocument()`: 202 | 203 | ```php 204 | // nothing will happen 205 | $document->first('head')->first('title')->remove(); 206 | 207 | // but this will do 208 | $document->first('head')->firstInDocument('title')->remove(); 209 | ``` 210 | 211 | **Warning:** methods `findInDocument()` and `firstInDocument()` work only for elements, which belong to a document, and for elements created via `new Element(...)`. If an element does not belong to a document, `LogicException` will be thrown; 212 | 213 | ## Supported selectors 214 | 215 | DiDom supports search by: 216 | 217 | - tag 218 | - class, ID, name and value of an attribute 219 | - pseudo-classes: 220 | - first-, last-, nth-child 221 | - empty and not-empty 222 | - contains 223 | - has 224 | 225 | ```php 226 | // all links 227 | $document->find('a'); 228 | 229 | // any element with id = "foo" and "bar" class 230 | $document->find('#foo.bar'); 231 | 232 | // any element with attribute "name" 233 | $document->find('[name]'); 234 | // the same as 235 | $document->find('*[name]'); 236 | 237 | // input field with the name "foo" 238 | $document->find('input[name=foo]'); 239 | $document->find('input[name=\'bar\']'); 240 | $document->find('input[name="baz"]'); 241 | 242 | // any element that has an attribute starting with "data-" and the value "foo" 243 | $document->find('*[^data-=foo]'); 244 | 245 | // all links starting with https 246 | $document->find('a[href^=https]'); 247 | 248 | // all images with the extension png 249 | $document->find('img[src$=png]'); 250 | 251 | // all links containing the string "example.com" 252 | $document->find('a[href*=example.com]'); 253 | 254 | // text of the links with "foo" class 255 | $document->find('a.foo::text'); 256 | 257 | // address and title of all the fields with "bar" class 258 | $document->find('a.bar::attr(href|title)'); 259 | ``` 260 | 261 | ## Changing content 262 | 263 | ### Change inner HTML 264 | 265 | ```php 266 | $element->setInnerHtml('Foo'); 267 | ``` 268 | 269 | ### Change inner XML 270 | 271 | ```php 272 | $element->setInnerXml(' Foo BarHello world! 274 | ]]>'); 275 | ``` 276 | 277 | ### Change value (as plain text) 278 | 279 | ```php 280 | $element->setValue('Foo'); 281 | // will be encoded like using htmlentities() 282 | $element->setValue('Foo'); 283 | ``` 284 | 285 | ## Output 286 | 287 | ### Getting HTML 288 | 289 | ##### With method `html()`: 290 | 291 | ```php 292 | $posts = $document->find('.post'); 293 | 294 | echo $posts[0]->html(); 295 | ``` 296 | 297 | ##### Casting to string: 298 | 299 | ```php 300 | $html = (string) $posts[0]; 301 | ``` 302 | 303 | ##### Formatting HTML output 304 | 305 | ```php 306 | $html = $document->format()->html(); 307 | ``` 308 | 309 | An element does not have `format()` method, so if you need to output formatted HTML of the element, then first you have to convert it to a document: 310 | 311 | 312 | ```php 313 | $html = $element->toDocument()->format()->html(); 314 | ``` 315 | 316 | #### Inner HTML 317 | 318 | ```php 319 | $innerHtml = $element->innerHtml(); 320 | ``` 321 | 322 | Document does not have the method `innerHtml()`, therefore, if you need to get inner HTML of a document, convert it into an element first: 323 | 324 | ```php 325 | $innerHtml = $document->toElement()->innerHtml(); 326 | ``` 327 | 328 | ### Getting XML 329 | 330 | ```php 331 | echo $document->xml(); 332 | 333 | echo $document->first('book')->xml(); 334 | ``` 335 | 336 | ### Getting content 337 | 338 | ```php 339 | $posts = $document->find('.post'); 340 | 341 | echo $posts[0]->text(); 342 | ``` 343 | 344 | ## Creating a new element 345 | 346 | ### Creating an instance of the class 347 | 348 | ```php 349 | use DiDom\Element; 350 | 351 | $element = new Element('span', 'Hello'); 352 | 353 | // Outputs "Hello" 354 | echo $element->html(); 355 | ``` 356 | 357 | First parameter is a name of an attribute, the second one is its value (optional), the third one is element attributes (optional). 358 | 359 | An example of creating an element with attributes: 360 | 361 | ```php 362 | $attributes = ['name' => 'description', 'placeholder' => 'Enter description of item']; 363 | 364 | $element = new Element('textarea', 'Text', $attributes); 365 | ``` 366 | 367 | An element can be created from an instance of the class `DOMElement`: 368 | 369 | ```php 370 | use DiDom\Element; 371 | use DOMElement; 372 | 373 | $domElement = new DOMElement('span', 'Hello'); 374 | 375 | $element = new Element($domElement); 376 | ``` 377 | 378 | ### Using the method `createElement` 379 | 380 | ```php 381 | $document = new Document($html); 382 | 383 | $element = $document->createElement('span', 'Hello'); 384 | ``` 385 | 386 | ## Getting the name of an element 387 | 388 | ```php 389 | $element->tagName(); 390 | ``` 391 | 392 | ## Getting parent element 393 | 394 | ```php 395 | $document = new Document($html); 396 | 397 | $input = $document->find('input[name=email]')[0]; 398 | 399 | var_dump($input->parent()); 400 | ``` 401 | 402 | ## Getting sibling elements 403 | 404 | ```php 405 | $document = new Document($html); 406 | 407 | $item = $document->find('ul.menu > li')[1]; 408 | 409 | var_dump($item->previousSibling()); 410 | 411 | var_dump($item->nextSibling()); 412 | ``` 413 | 414 | ## Getting the child elements 415 | 416 | ```php 417 | $html = '
FooBar
'; 418 | 419 | $document = new Document($html); 420 | 421 | $div = $document->first('div'); 422 | 423 | // element node (DOMElement) 424 | // string(3) "Bar" 425 | var_dump($div->child(1)->text()); 426 | 427 | // text node (DOMText) 428 | // string(3) "Foo" 429 | var_dump($div->firstChild()->text()); 430 | 431 | // comment node (DOMComment) 432 | // string(3) "Baz" 433 | var_dump($div->lastChild()->text()); 434 | 435 | // array(3) { ... } 436 | var_dump($div->children()); 437 | ``` 438 | 439 | ## Getting owner document 440 | 441 | ```php 442 | $document = new Document($html); 443 | 444 | $element = $document->find('input[name=email]')[0]; 445 | 446 | $document2 = $element->ownerDocument(); 447 | 448 | // bool(true) 449 | var_dump($document->is($document2)); 450 | ``` 451 | 452 | ## Working with element attributes 453 | 454 | #### Creating/updating an attribute 455 | 456 | ##### With method `setAttribute`: 457 | ```php 458 | $element->setAttribute('name', 'username'); 459 | ``` 460 | 461 | ##### With method `attr`: 462 | ```php 463 | $element->attr('name', 'username'); 464 | ``` 465 | 466 | ##### With magic method `__set`: 467 | ```php 468 | $element->name = 'username'; 469 | ``` 470 | 471 | #### Getting value of an attribute 472 | 473 | ##### With method `getAttribute`: 474 | 475 | ```php 476 | $username = $element->getAttribute('value'); 477 | ``` 478 | 479 | ##### With method `attr`: 480 | 481 | ```php 482 | $username = $element->attr('value'); 483 | ``` 484 | 485 | ##### With magic method `__get`: 486 | 487 | ```php 488 | $username = $element->name; 489 | ``` 490 | 491 | Returns `null` if attribute is not found. 492 | 493 | #### Verify if attribute exists 494 | 495 | ##### With method `hasAttribute`: 496 | 497 | ```php 498 | if ($element->hasAttribute('name')) { 499 | // code 500 | } 501 | ``` 502 | 503 | ##### With magic method `__isset`: 504 | 505 | ```php 506 | if (isset($element->name)) { 507 | // code 508 | } 509 | ``` 510 | 511 | #### Removing attribute: 512 | 513 | ##### With method `removeAttribute`: 514 | 515 | ```php 516 | $element->removeAttribute('name'); 517 | ``` 518 | 519 | ##### With magic method `__unset`: 520 | 521 | ```php 522 | unset($element->name); 523 | ``` 524 | 525 | ## Comparing elements 526 | 527 | ```php 528 | $element = new Element('span', 'hello'); 529 | $element2 = new Element('span', 'hello'); 530 | 531 | // bool(true) 532 | var_dump($element->is($element)); 533 | 534 | // bool(false) 535 | var_dump($element->is($element2)); 536 | ``` 537 | 538 | ## Appending child elements 539 | 540 | ```php 541 | $list = new Element('ul'); 542 | 543 | $item = new Element('li', 'Item 1'); 544 | 545 | $list->appendChild($item); 546 | 547 | $items = [ 548 | new Element('li', 'Item 2'), 549 | new Element('li', 'Item 3'), 550 | ]; 551 | 552 | $list->appendChild($items); 553 | ``` 554 | 555 | ## Adding a child element 556 | 557 | ```php 558 | $list = new Element('ul'); 559 | 560 | $item = new Element('li', 'Item 1'); 561 | $items = [ 562 | new Element('li', 'Item 2'), 563 | new Element('li', 'Item 3'), 564 | ]; 565 | 566 | $list->appendChild($item); 567 | $list->appendChild($items); 568 | ``` 569 | 570 | ## Replacing element 571 | 572 | ```php 573 | $element = new Element('span', 'hello'); 574 | 575 | $document->find('.post')[0]->replace($element); 576 | ``` 577 | 578 | **Waning:** you can replace only those elements that were found directly in the document: 579 | 580 | ```php 581 | // nothing will happen 582 | $document->first('head')->first('title')->replace($title); 583 | 584 | // but this will do 585 | $document->first('head title')->replace($title); 586 | ``` 587 | 588 | More about this in section [Search for elements](#search-for-elements). 589 | 590 | ## Removing element 591 | 592 | ```php 593 | $document->find('.post')[0]->remove(); 594 | ``` 595 | 596 | **Warning:** you can remove only those elements that were found directly in the document: 597 | 598 | ```php 599 | // nothing will happen 600 | $document->first('head')->first('title')->remove(); 601 | 602 | // but this will do 603 | $document->first('head title')->remove(); 604 | ``` 605 | 606 | More about this in section [Search for elements](#search-for-elements). 607 | 608 | ## Working with cache 609 | 610 | Cache is an array of XPath expressions, that were converted from CSS. 611 | 612 | #### Getting from cache 613 | 614 | ```php 615 | use DiDom\Query; 616 | 617 | ... 618 | 619 | $xpath = Query::compile('h2'); 620 | $compiled = Query::getCompiled(); 621 | 622 | // array('h2' => '//h2') 623 | var_dump($compiled); 624 | ``` 625 | 626 | #### Cache setting 627 | 628 | ```php 629 | Query::setCompiled(['h2' => '//h2']); 630 | ``` 631 | 632 | ## Miscellaneous 633 | 634 | #### `preserveWhiteSpace` 635 | 636 | By default, whitespace preserving is disabled. 637 | 638 | You can enable the `preserveWhiteSpace` option before loading the document: 639 | 640 | ```php 641 | $document = new Document(); 642 | 643 | $document->preserveWhiteSpace(); 644 | 645 | $document->loadXml($xml); 646 | ``` 647 | 648 | #### `count` 649 | 650 | The `count ()` method counts children that match the selector: 651 | 652 | ```php 653 | // prints the number of links in the document 654 | echo $document->count('a'); 655 | ``` 656 | 657 | ```php 658 | // prints the number of items in the list 659 | echo $document->first('ul')->count('li'); 660 | ``` 661 | 662 | #### `matches` 663 | 664 | Returns `true` if the node matches the selector: 665 | 666 | ```php 667 | $element->matches('div#content'); 668 | 669 | // strict match 670 | // returns true if the element is a div with id equals content and nothing else 671 | // if the element has any other attributes the method returns false 672 | $element->matches('div#content', true); 673 | ``` 674 | 675 | #### `isElementNode` 676 | 677 | Checks whether an element is an element (DOMElement): 678 | 679 | ```php 680 | $element->isElementNode(); 681 | ``` 682 | 683 | #### `isTextNode` 684 | 685 | Checks whether an element is a text node (DOMText): 686 | 687 | ```php 688 | $element->isTextNode(); 689 | ``` 690 | 691 | #### `isCommentNode` 692 | 693 | Checks whether the element is a comment (DOMComment): 694 | 695 | ```php 696 | $element->isCommentNode(); 697 | ``` 698 | 699 | ## Comparison with other parsers 700 | 701 | [Comparison with other parsers](https://github.com/Imangazaliev/DiDOM/wiki/Comparison-with-other-parsers-(1.0)) 702 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imangazaliev/didom", 3 | "description": "Simple and fast HTML parser", 4 | "type": "library", 5 | "keywords": ["didom", "parser", "html", "xml"], 6 | "license": "MIT", 7 | "homepage": "https://github.com/Imangazaliev/DiDOM", 8 | "authors": [ 9 | { 10 | "name": "Imangazaliev Muhammad", 11 | "email": "imangazalievm@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.2", 16 | "ext-dom": "*", 17 | "ext-iconv": "*" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^8.5" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "DiDom\\": "src/DiDom/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "DiDom\\Tests\\": "tests/" 30 | } 31 | }, 32 | "config": { 33 | "platform": { 34 | "php": "7.2" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/src/DiDom/ClassAttribute.php: -------------------------------------------------------------------------------- 1 | isElementNode()) { 36 | throw new InvalidArgumentException(sprintf('The element must contain DOMElement node.')); 37 | } 38 | 39 | $this->element = $element; 40 | 41 | $this->parseClassAttribute(); 42 | } 43 | 44 | /** 45 | * Parses class attribute of the element. 46 | */ 47 | protected function parseClassAttribute() 48 | { 49 | if ( ! $this->element->hasAttribute('class')) { 50 | // possible if class attribute has been removed 51 | if ($this->classesString !== '') { 52 | $this->classesString = ''; 53 | $this->classes = []; 54 | } 55 | 56 | return; 57 | } 58 | 59 | // if class attribute is not changed 60 | if ($this->element->getAttribute('class') === $this->classesString) { 61 | return; 62 | } 63 | 64 | // save class attribute as is (without trimming) 65 | $this->classesString = $this->element->getAttribute('class'); 66 | 67 | $classesString = trim($this->classesString); 68 | 69 | if ($classesString === '') { 70 | $this->classes = []; 71 | 72 | return; 73 | } 74 | 75 | $classes = explode(' ', $classesString); 76 | 77 | $classes = array_map('trim', $classes); 78 | $classes = array_filter($classes); 79 | $classes = array_unique($classes); 80 | 81 | $this->classes = array_values($classes); 82 | } 83 | 84 | /** 85 | * Updates class attribute of the element. 86 | */ 87 | protected function updateClassAttribute() 88 | { 89 | $this->classesString = implode(' ', $this->classes); 90 | 91 | $this->element->setAttribute('class', $this->classesString); 92 | } 93 | 94 | /** 95 | * @param string $className 96 | * 97 | * @return ClassAttribute 98 | * 99 | * @throws InvalidArgumentException if class name is not a string 100 | */ 101 | public function add(string $className): self 102 | { 103 | $this->parseClassAttribute(); 104 | 105 | if (in_array($className, $this->classes, true)) { 106 | return $this; 107 | } 108 | 109 | $this->classes[] = $className; 110 | 111 | $this->updateClassAttribute(); 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * @param array $classNames 118 | * 119 | * @return ClassAttribute 120 | * 121 | * @throws InvalidArgumentException if class name is not a string 122 | */ 123 | public function addMultiple(array $classNames): self 124 | { 125 | $this->parseClassAttribute(); 126 | 127 | foreach ($classNames as $className) { 128 | if ( ! is_string($className)) { 129 | throw new InvalidArgumentException(sprintf('Class name must be a string, %s given.', (is_object($className) ? get_class($className) : gettype($className)))); 130 | } 131 | 132 | if (in_array($className, $this->classes, true)) { 133 | continue; 134 | } 135 | 136 | $this->classes[] = $className; 137 | } 138 | 139 | $this->updateClassAttribute(); 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * @return string[] 146 | */ 147 | public function getAll(): array 148 | { 149 | $this->parseClassAttribute(); 150 | 151 | return $this->classes; 152 | } 153 | 154 | /** 155 | * @param string $className 156 | * 157 | * @return bool 158 | */ 159 | public function contains(string $className): bool 160 | { 161 | $this->parseClassAttribute(); 162 | 163 | return in_array($className, $this->classes, true); 164 | } 165 | 166 | /** 167 | * @param string $className 168 | * 169 | * @return ClassAttribute 170 | * 171 | * @throws InvalidArgumentException if class name is not a string 172 | */ 173 | public function remove(string $className): self 174 | { 175 | $this->parseClassAttribute(); 176 | 177 | $classIndex = array_search($className, $this->classes); 178 | 179 | if ($classIndex === false) { 180 | return $this; 181 | } 182 | 183 | unset($this->classes[$classIndex]); 184 | 185 | $this->updateClassAttribute(); 186 | 187 | return $this; 188 | } 189 | 190 | /** 191 | * @param array $classNames 192 | * 193 | * @return ClassAttribute 194 | * 195 | * @throws InvalidArgumentException if class name is not a string 196 | */ 197 | public function removeMultiple(array $classNames): self 198 | { 199 | $this->parseClassAttribute(); 200 | 201 | foreach ($classNames as $className) { 202 | if ( ! is_string($className)) { 203 | throw new InvalidArgumentException(sprintf('Class name must be a string, %s given.', (is_object($className) ? get_class($className) : gettype($className)))); 204 | } 205 | 206 | $classIndex = array_search($className, $this->classes); 207 | 208 | if ($classIndex === false) { 209 | continue; 210 | } 211 | 212 | unset($this->classes[$classIndex]); 213 | } 214 | 215 | $this->updateClassAttribute(); 216 | 217 | return $this; 218 | } 219 | 220 | /** 221 | * @param string[] $preserved 222 | * 223 | * @return ClassAttribute 224 | */ 225 | public function removeAll(array $preserved = []): self 226 | { 227 | $this->parseClassAttribute(); 228 | 229 | $preservedClasses = []; 230 | 231 | foreach ($preserved as $className) { 232 | if ( ! is_string($className)) { 233 | throw new InvalidArgumentException(sprintf('Class name must be a string, %s given.', (is_object($className) ? get_class($className) : gettype($className)))); 234 | } 235 | 236 | if ( ! in_array($className, $this->classes, true)) { 237 | continue; 238 | } 239 | 240 | $preservedClasses[] = $className; 241 | } 242 | 243 | $this->classes = $preservedClasses; 244 | 245 | $this->updateClassAttribute(); 246 | 247 | return $this; 248 | } 249 | 250 | /** 251 | * @return Element 252 | */ 253 | public function getElement(): Element 254 | { 255 | return $this->element; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/src/DiDom/Document.php: -------------------------------------------------------------------------------- 1 | 'http://php.net/xpath' 50 | ]; 51 | 52 | /** 53 | * @param DOMDocument|string|null $string An HTML or XML string, a file path or a DOMDocument instance 54 | * @param bool $isFile Indicates that the first parameter is a path to a file 55 | * @param string $encoding The document encoding 56 | * @param string $type The document type 57 | * 58 | * @throws InvalidArgumentException if parameter 3 is not a string 59 | */ 60 | public function __construct($string = null, bool $isFile = false, string $encoding = 'UTF-8', string $type = Document::TYPE_HTML) 61 | { 62 | if ($string instanceof DOMDocument) { 63 | $this->document = $string; 64 | 65 | return; 66 | } 67 | 68 | $this->encoding = $encoding; 69 | 70 | $this->document = new DOMDocument('1.0', $encoding); 71 | 72 | $this->preserveWhiteSpace(false); 73 | 74 | if ($string !== null) { 75 | $this->load($string, $isFile, $type); 76 | } 77 | } 78 | 79 | /** 80 | * Creates a new document. 81 | * 82 | * @param DOMDocument|string|null $string An HTML or XML string, a file path or a DOMDocument instance 83 | * @param bool $isFile Indicates that the first parameter is a path to a file 84 | * @param string $encoding The document encoding 85 | * @param string $type The document type 86 | * 87 | * @return Document 88 | */ 89 | public static function create($string = null, bool $isFile = false, string $encoding = 'UTF-8', string $type = Document::TYPE_HTML) 90 | { 91 | return new Document($string, $isFile, $encoding, $type); 92 | } 93 | 94 | /** 95 | * Creates a new element node. 96 | * 97 | * @param string $name The tag name of the element 98 | * @param string|null $value The value of the element 99 | * @param array $attributes The attributes of the element 100 | * 101 | * @return Element created element 102 | */ 103 | public function createElement(string $name, ?string $value = null, array $attributes = []): Element 104 | { 105 | $node = $this->document->createElement($name); 106 | 107 | return new Element($node, $value, $attributes); 108 | } 109 | 110 | /** 111 | * Creates a new element node by CSS selector. 112 | * 113 | * @param string $selector 114 | * @param string|null $value 115 | * @param array $attributes 116 | * 117 | * @return Element 118 | * 119 | * @throws InvalidSelectorException 120 | */ 121 | public function createElementBySelector(string $selector, ?string $value = null, array $attributes = []): Element 122 | { 123 | $segments = Query::getSegments($selector); 124 | 125 | $name = array_key_exists('tag', $segments) ? $segments['tag'] : 'div'; 126 | 127 | if (array_key_exists('attributes', $segments)) { 128 | $attributes = array_merge($attributes, $segments['attributes']); 129 | } 130 | 131 | if (array_key_exists('id', $segments)) { 132 | $attributes['id'] = $segments['id']; 133 | } 134 | 135 | if (array_key_exists('classes', $segments)) { 136 | $attributes['class'] = implode(' ', $segments['classes']); 137 | } 138 | 139 | return $this->createElement($name, $value, $attributes); 140 | } 141 | 142 | /** 143 | * @param string $content 144 | * 145 | * @return Element 146 | */ 147 | public function createTextNode(string $content): Element 148 | { 149 | return new Element(new DOMText($content)); 150 | } 151 | 152 | /** 153 | * @param string $data 154 | * 155 | * @return Element 156 | */ 157 | public function createComment(string $data): Element 158 | { 159 | return new Element(new DOMComment($data)); 160 | } 161 | 162 | /** 163 | * @param string $data 164 | * 165 | * @return Element 166 | */ 167 | public function createCdataSection(string $data): Element 168 | { 169 | return new Element(new DOMCdataSection($data)); 170 | } 171 | 172 | /** 173 | * @return DocumentFragment 174 | */ 175 | public function createDocumentFragment(): DocumentFragment 176 | { 177 | return new DocumentFragment($this->document->createDocumentFragment()); 178 | } 179 | 180 | /** 181 | * Adds a new child at the end of the children. 182 | * 183 | * @param Element|DOMNode|array $nodes The appended child 184 | * 185 | * @return Element|Element[] 186 | * 187 | * @throws InvalidArgumentException if one of elements of parameter 1 is not an instance of DOMNode or Element 188 | */ 189 | public function appendChild($nodes) 190 | { 191 | $returnArray = true; 192 | 193 | if ( ! is_array($nodes)) { 194 | $nodes = [$nodes]; 195 | 196 | $returnArray = false; 197 | } 198 | 199 | $result = []; 200 | 201 | foreach ($nodes as $node) { 202 | if ($node instanceof Element) { 203 | $node = $node->getNode(); 204 | } 205 | 206 | if ( ! $node instanceof DOMNode) { 207 | throw new InvalidArgumentException(sprintf('Argument 1 passed to %s must be an instance of %s\Element or DOMNode, %s given.', __METHOD__, __NAMESPACE__, (is_object($node) ? get_class($node) : gettype($node)))); 208 | } 209 | 210 | Errors::disable(); 211 | 212 | $cloned = $node->cloneNode(true); 213 | $newNode = $this->document->importNode($cloned, true); 214 | 215 | $result[] = $this->document->appendChild($newNode); 216 | 217 | Errors::restore(); 218 | } 219 | 220 | $result = array_map(function (DOMNode $node) { 221 | return new Element($node); 222 | }, $result); 223 | 224 | return $returnArray ? $result : $result[0]; 225 | } 226 | 227 | /** 228 | * Set preserveWhiteSpace property. 229 | * 230 | * @param bool $value 231 | * 232 | * @return Document 233 | */ 234 | public function preserveWhiteSpace(bool $value = true): self 235 | { 236 | $this->document->preserveWhiteSpace = $value; 237 | 238 | return $this; 239 | } 240 | 241 | /** 242 | * Load HTML or XML. 243 | * 244 | * @param string $string An HTML or XML string or a file path 245 | * @param bool $isFile Indicates that the first parameter is a file path 246 | * @param string $type The type of a document 247 | * @param int|null $options libxml option constants 248 | * 249 | * @throws InvalidArgumentException if parameter 1 is not a string 250 | * @throws InvalidArgumentException if parameter 3 is not a string 251 | * @throws InvalidArgumentException if parameter 4 is not an integer or null 252 | * @throws RuntimeException if the document type is invalid (not Document::TYPE_HTML or Document::TYPE_XML) 253 | */ 254 | public function load(string $string, bool $isFile = false, string $type = Document::TYPE_HTML, int $options = null): void 255 | { 256 | if ( ! in_array(strtolower($type), [Document::TYPE_HTML, Document::TYPE_XML], true)) { 257 | throw new RuntimeException(sprintf('Document type must be "xml" or "html", %s given.', $type)); 258 | } 259 | 260 | if ($options === null) { 261 | // LIBXML_HTML_NODEFDTD - prevents a default doctype being added when one is not found 262 | $options = LIBXML_HTML_NODEFDTD; 263 | } 264 | 265 | $string = trim($string); 266 | 267 | if ($isFile) { 268 | $string = $this->loadFile($string); 269 | } 270 | 271 | if (strtolower($type) === Document::TYPE_HTML) { 272 | $string = Encoder::convertToHtmlEntities($string, $this->encoding); 273 | } 274 | 275 | $this->type = strtolower($type); 276 | 277 | Errors::disable(); 278 | 279 | if ($this->type === Document::TYPE_HTML) { 280 | $this->document->loadHtml($string, $options); 281 | } else { 282 | $this->document->loadXml($string, $options); 283 | } 284 | 285 | Errors::restore(); 286 | } 287 | 288 | /** 289 | * Load HTML from a string. 290 | * 291 | * @param string $html The HTML string 292 | * @param int|null $options Additional parameters 293 | * 294 | * @return Document 295 | * 296 | * @throws InvalidArgumentException if parameter 1 is not a string 297 | */ 298 | public function loadHtml(string $html, ?int $options = null): void 299 | { 300 | $this->load($html, false, Document::TYPE_HTML, $options); 301 | } 302 | 303 | /** 304 | * Load HTML from a file. 305 | * 306 | * @param string $filename The path to the HTML file 307 | * @param int|null $options Additional parameters 308 | * 309 | * @throws InvalidArgumentException if parameter 1 not a string 310 | * @throws RuntimeException if the file doesn't exist 311 | * @throws RuntimeException if you are unable to load the file 312 | */ 313 | public function loadHtmlFile(string $filename, ?int $options = null): void 314 | { 315 | $this->load($filename, true, Document::TYPE_HTML, $options); 316 | } 317 | 318 | /** 319 | * Load XML from a string. 320 | * 321 | * @param string $xml The XML string 322 | * @param int|null $options Additional parameters 323 | * 324 | * @throws InvalidArgumentException if parameter 1 is not a string 325 | */ 326 | public function loadXml(string $xml, ?int $options = null): void 327 | { 328 | $this->load($xml, false, Document::TYPE_XML, $options); 329 | } 330 | 331 | /** 332 | * Load XML from a file. 333 | * 334 | * @param string $filename The path to the XML file 335 | * @param int|null $options Additional parameters 336 | * 337 | * @throws InvalidArgumentException if the file path is not a string 338 | * @throws RuntimeException if the file doesn't exist 339 | * @throws RuntimeException if you are unable to load the file 340 | */ 341 | public function loadXmlFile(string $filename, ?int $options = null): void 342 | { 343 | $this->load($filename, true, Document::TYPE_XML, $options); 344 | } 345 | 346 | /** 347 | * Reads entire file into a string. 348 | * 349 | * @param string $filename The path to the file 350 | * 351 | * @return string 352 | * 353 | * @throws InvalidArgumentException if parameter 1 is not a string 354 | * @throws RuntimeException if an error occurred 355 | */ 356 | protected function loadFile(string $filename): string 357 | { 358 | try { 359 | $content = file_get_contents($filename); 360 | } catch (Exception $exception) { 361 | throw new RuntimeException(sprintf('Could not load file %s.', $filename)); 362 | } 363 | 364 | if ($content === false) { 365 | throw new RuntimeException(sprintf('Could not load file %s.', $filename)); 366 | } 367 | 368 | return $content; 369 | } 370 | 371 | /** 372 | * Checks the existence of the node. 373 | * 374 | * @param string $expression XPath expression or CSS selector 375 | * @param string $type The type of the expression 376 | * 377 | * @return bool 378 | */ 379 | public function has(string $expression, string $type = Query::TYPE_CSS): bool 380 | { 381 | $expression = Query::compile($expression, $type); 382 | $expression = sprintf('count(%s) > 0', $expression); 383 | 384 | return $this->createXpath()->evaluate($expression); 385 | } 386 | 387 | /** 388 | * Searches for a node in the DOM tree for a given XPath expression or CSS selector. 389 | * 390 | * @param string $expression XPath expression or a CSS selector 391 | * @param string $type The type of the expression 392 | * @param bool $wrapNode Returns array of Element if true, otherwise array of DOMElement 393 | * @param Element|DOMElement|null $contextNode The node in which the search will be performed 394 | * 395 | * @return Element[]|DOMElement[] 396 | * 397 | * @throws InvalidSelectorException if the selector is invalid 398 | * @throws InvalidArgumentException if context node is not DOMElement 399 | */ 400 | public function find(string $expression, string $type = Query::TYPE_CSS, bool $wrapNode = true, $contextNode = null): array 401 | { 402 | $expression = Query::compile($expression, $type); 403 | 404 | if ($contextNode !== null) { 405 | if ($contextNode instanceof Element) { 406 | $contextNode = $contextNode->getNode(); 407 | } 408 | 409 | if ( ! $contextNode instanceof DOMElement) { 410 | throw new InvalidArgumentException(sprintf('Argument 4 passed to %s must be an instance of %s\Element or DOMElement, %s given.', __METHOD__, __NAMESPACE__, (is_object($contextNode) ? get_class($contextNode) : gettype($contextNode)))); 411 | } 412 | 413 | if ($type === Query::TYPE_CSS) { 414 | $expression = '.' . $expression; 415 | } 416 | } 417 | 418 | $nodeList = $this->createXpath()->query($expression, $contextNode); 419 | 420 | $result = []; 421 | 422 | if ($wrapNode) { 423 | foreach ($nodeList as $node) { 424 | $result[] = $this->wrapNode($node); 425 | } 426 | } else { 427 | foreach ($nodeList as $node) { 428 | $result[] = $node; 429 | } 430 | } 431 | 432 | return $result; 433 | } 434 | 435 | /** 436 | * Searches for a node in the DOM tree and returns first element or null. 437 | * 438 | * @param string $expression XPath expression or a CSS selector 439 | * @param string $type The type of the expression 440 | * @param bool $wrapNode Returns array of Element if true, otherwise array of DOMElement 441 | * @param Element|DOMElement|null $contextNode The node in which the search will be performed 442 | * 443 | * @return Element|DOMElement|null 444 | * 445 | * @throws InvalidSelectorException if the selector is invalid 446 | */ 447 | public function first(string $expression, string $type = Query::TYPE_CSS, bool $wrapNode = true, $contextNode = null) 448 | { 449 | $expression = Query::compile($expression, $type); 450 | 451 | if ($contextNode !== null && $type === Query::TYPE_CSS) { 452 | $expression = '.' . $expression; 453 | } 454 | 455 | $expression = sprintf('(%s)[1]', $expression); 456 | 457 | $nodes = $this->find($expression, Query::TYPE_XPATH, false, $contextNode); 458 | 459 | if (count($nodes) === 0) { 460 | return null; 461 | } 462 | 463 | return $wrapNode ? $this->wrapNode($nodes[0]) : $nodes[0]; 464 | } 465 | 466 | /** 467 | * @param DOMElement|DOMText|DOMAttr $node 468 | * 469 | * @return Element|string 470 | * 471 | * @throws InvalidArgumentException if parameter 1 is not an instance of DOMElement, DOMText, DOMComment, DOMCdataSection or DOMAttr 472 | */ 473 | protected function wrapNode($node) 474 | { 475 | switch (get_class($node)) { 476 | case 'DOMElement': 477 | case 'DOMComment': 478 | case 'DOMCdataSection': 479 | return new Element($node); 480 | 481 | case 'DOMText': 482 | return $node->data; 483 | 484 | case 'DOMAttr': 485 | return $node->value; 486 | } 487 | 488 | throw new InvalidArgumentException(sprintf('Unknown node type "%s".', get_class($node))); 489 | } 490 | 491 | /** 492 | * Searches for a node in the DOM tree for a given XPath expression. 493 | * 494 | * @param string $expression XPath expression 495 | * @param bool $wrapNode Returns array of Element if true, otherwise array of DOMElement 496 | * @param Element|DOMElement $contextNode The node in which the search will be performed 497 | * 498 | * @return Element[]|DOMElement[] 499 | */ 500 | public function xpath(string $expression, bool $wrapNode = true, $contextNode = null): array 501 | { 502 | return $this->find($expression, Query::TYPE_XPATH, $wrapNode, $contextNode); 503 | } 504 | 505 | /** 506 | * Counts nodes for a given XPath expression or CSS selector. 507 | * 508 | * @param string $expression XPath expression or CSS selector 509 | * @param string $type The type of the expression 510 | * 511 | * @return int 512 | * 513 | * @throws InvalidSelectorException 514 | */ 515 | public function count(string $expression, string $type = Query::TYPE_CSS): int 516 | { 517 | $expression = Query::compile($expression, $type); 518 | $expression = sprintf('count(%s)', $expression); 519 | 520 | return (int) $this->createXpath()->evaluate($expression); 521 | } 522 | 523 | /** 524 | * @return DOMXPath 525 | */ 526 | public function createXpath(): DOMXPath 527 | { 528 | $xpath = new DOMXPath($this->document); 529 | 530 | foreach ($this->namespaces as $prefix => $namespace) { 531 | $xpath->registerNamespace($prefix, $namespace); 532 | } 533 | 534 | $xpath->registerPhpFunctions(); 535 | 536 | return $xpath; 537 | } 538 | 539 | /** 540 | * Register a namespace. 541 | * 542 | * @param string $prefix 543 | * @param string $namespace 544 | */ 545 | public function registerNamespace(string $prefix, string $namespace) 546 | { 547 | $this->namespaces[$prefix] = $namespace; 548 | } 549 | 550 | /** 551 | * Dumps the internal document into a string using HTML formatting. 552 | * 553 | * @return string The document html 554 | */ 555 | public function html(): string 556 | { 557 | return trim($this->document->saveHTML($this->document)); 558 | } 559 | 560 | /** 561 | * Dumps the internal document into a string using XML formatting. 562 | * 563 | * @param int|null $options Additional options 564 | * 565 | * @return string The document xml 566 | */ 567 | public function xml(?int $options = 0): string 568 | { 569 | return trim($this->document->saveXML($this->document, $options)); 570 | } 571 | 572 | /** 573 | * Nicely formats output with indentation and extra space. 574 | * 575 | * @param bool $format Formats output if true 576 | * 577 | * @return Document 578 | */ 579 | public function format(bool $format = true): self 580 | { 581 | $this->document->formatOutput = $format; 582 | 583 | return $this; 584 | } 585 | 586 | /** 587 | * Get the text content of this node and its descendants. 588 | * 589 | * @return string 590 | */ 591 | public function text(): string 592 | { 593 | return $this->getElement()->textContent; 594 | } 595 | 596 | /** 597 | * Indicates if two documents are the same document. 598 | * 599 | * @param Document|DOMDocument $document The compared document 600 | * 601 | * @return bool 602 | * 603 | * @throws InvalidArgumentException if parameter 1 is not an instance of DOMDocument or Document 604 | */ 605 | public function is($document): bool 606 | { 607 | if ($document instanceof Document) { 608 | $element = $document->getElement(); 609 | } else { 610 | if ( ! $document instanceof DOMDocument) { 611 | throw new InvalidArgumentException(sprintf('Argument 1 passed to %s must be an instance of %s or DOMDocument, %s given.', __METHOD__, __CLASS__, (is_object($document) ? get_class($document) : gettype($document)))); 612 | } 613 | 614 | $element = $document->documentElement; 615 | } 616 | 617 | if ($element === null) { 618 | return false; 619 | } 620 | 621 | return $this->getElement()->isSameNode($element); 622 | } 623 | 624 | /** 625 | * Returns the type of the document (XML or HTML). 626 | * 627 | * @return string|null 628 | */ 629 | public function getType(): ?string 630 | { 631 | return $this->type; 632 | } 633 | 634 | /** 635 | * Returns the encoding of the document. 636 | * 637 | * @return string|null 638 | */ 639 | public function getEncoding(): ?string 640 | { 641 | return $this->encoding; 642 | } 643 | 644 | /** 645 | * @return DOMDocument 646 | */ 647 | public function getDocument(): DOMDocument 648 | { 649 | return $this->document; 650 | } 651 | 652 | /** 653 | * @return DOMElement|null 654 | */ 655 | public function getElement(): ?DOMElement 656 | { 657 | return $this->document->documentElement; 658 | } 659 | 660 | /** 661 | * @return Element 662 | */ 663 | public function toElement(): Element 664 | { 665 | if ($this->document->documentElement === null) { 666 | throw new RuntimeException('Cannot convert empty document to Element.'); 667 | } 668 | 669 | return new Element($this->document->documentElement); 670 | } 671 | 672 | /** 673 | * Convert the document to its string representation. 674 | * 675 | * @return string 676 | */ 677 | public function __toString(): string 678 | { 679 | return $this->type === Document::TYPE_HTML ? $this->html() : $this->xml(); 680 | } 681 | } 682 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/src/DiDom/DocumentFragment.php: -------------------------------------------------------------------------------- 1 | setNode($documentFragment); 20 | } 21 | 22 | /** 23 | * Append raw XML data. 24 | * 25 | * @param string $data 26 | */ 27 | public function appendXml($data) 28 | { 29 | $this->node->appendXML($data); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/src/DiDom/Element.php: -------------------------------------------------------------------------------- 1 | createElement($tagName); 41 | 42 | $this->setNode($node); 43 | } else { 44 | $this->setNode($tagName); 45 | } 46 | 47 | if ($value !== null) { 48 | $this->setValue($value); 49 | } 50 | 51 | foreach ($attributes as $attrName => $attrValue) { 52 | $this->setAttribute($attrName, $attrValue); 53 | } 54 | } 55 | 56 | /** 57 | * Creates a new element. 58 | * 59 | * @param DOMNode|string $name The tag name of an element 60 | * @param string|integer|float|null $value The value of an element 61 | * @param array $attributes The attributes of an element 62 | * 63 | * @return Element 64 | */ 65 | public static function create($name, $value = null, array $attributes = []): self 66 | { 67 | return new Element($name, $value, $attributes); 68 | } 69 | 70 | /** 71 | * Creates a new element node by CSS selector. 72 | * 73 | * @param string $selector 74 | * @param string|null $value 75 | * @param array $attributes 76 | * 77 | * @return Element 78 | * 79 | * @throws InvalidSelectorException 80 | */ 81 | public static function createBySelector(string $selector, ?string $value = null, array $attributes = []): self 82 | { 83 | return Document::create()->createElementBySelector($selector, $value, $attributes); 84 | } 85 | 86 | public function tagName(): string 87 | { 88 | return $this->node->tagName; 89 | } 90 | 91 | /** 92 | * Checks that the node matches selector. 93 | * 94 | * @param string $selector CSS selector 95 | * @param bool $strict 96 | * 97 | * @return bool 98 | * 99 | * @throws InvalidSelectorException if the selector is invalid 100 | * @throws InvalidArgumentException if the tag name is not a string 101 | * @throws RuntimeException if the tag name is not specified in strict mode 102 | */ 103 | public function matches(string $selector, bool $strict = false): bool 104 | { 105 | if ( ! $this->node instanceof DOMElement) { 106 | return false; 107 | } 108 | 109 | if ($selector === '*') { 110 | return true; 111 | } 112 | 113 | if ( ! $strict) { 114 | $innerHtml = $this->html(); 115 | $html = "$innerHtml"; 116 | 117 | $selector = 'root > ' . trim($selector); 118 | 119 | $document = new Document(); 120 | 121 | $document->loadHtml($html, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED); 122 | 123 | return $document->has($selector); 124 | } 125 | 126 | $segments = Query::getSegments($selector); 127 | 128 | if ( ! array_key_exists('tag', $segments)) { 129 | throw new RuntimeException(sprintf('Tag name must be specified in %s', $selector)); 130 | } 131 | 132 | if ($segments['tag'] !== $this->tagName() && $segments['tag'] !== '*') { 133 | return false; 134 | } 135 | 136 | $segments['id'] = array_key_exists('id', $segments) ? $segments['id'] : null; 137 | 138 | if ($segments['id'] !== $this->getAttribute('id')) { 139 | return false; 140 | } 141 | 142 | $classes = $this->hasAttribute('class') ? explode(' ', trim($this->getAttribute('class'))) : []; 143 | 144 | $segments['classes'] = array_key_exists('classes', $segments) ? $segments['classes'] : []; 145 | 146 | $diff1 = array_diff($segments['classes'], $classes); 147 | $diff2 = array_diff($classes, $segments['classes']); 148 | 149 | if (count($diff1) > 0 || count($diff2) > 0) { 150 | return false; 151 | } 152 | 153 | $attributes = $this->attributes(); 154 | 155 | unset($attributes['id'], $attributes['class']); 156 | 157 | $segments['attributes'] = array_key_exists('attributes', $segments) ? $segments['attributes'] : []; 158 | 159 | $diff1 = array_diff_assoc($segments['attributes'], $attributes); 160 | $diff2 = array_diff_assoc($attributes, $segments['attributes']); 161 | 162 | // if the attributes are not equal 163 | if (count($diff1) > 0 || count($diff2) > 0) { 164 | return false; 165 | } 166 | 167 | return true; 168 | } 169 | 170 | /** 171 | * Determine if an attribute exists on the element. 172 | * 173 | * @param string $name The name of an attribute 174 | * 175 | * @return bool 176 | */ 177 | public function hasAttribute(string $name): bool 178 | { 179 | return $this->node->hasAttribute($name); 180 | } 181 | 182 | /** 183 | * Set an attribute on the element. 184 | * 185 | * @param string $name The name of an attribute 186 | * @param string|integer|float $value The value of an attribute 187 | * 188 | * @return Element 189 | */ 190 | public function setAttribute(string $name, $value): Element 191 | { 192 | if (is_numeric($value)) { 193 | $value = (string) $value; 194 | } 195 | 196 | if ( ! is_string($value)) { 197 | throw new InvalidArgumentException(sprintf('%s expects parameter 2 to be string or null, %s given.', __METHOD__, (is_object($value) ? get_class($value) : gettype($value)))); 198 | } 199 | 200 | $this->node->setAttribute($name, $value); 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * Access to the element's attributes. 207 | * 208 | * @param string $name The name of an attribute 209 | * @param string|null $default The value returned if the attribute doesn't exist 210 | * 211 | * @return string|null The value of an attribute or null if attribute doesn't exist 212 | */ 213 | public function getAttribute(string $name, ?string $default = null): ?string 214 | { 215 | if ($this->hasAttribute($name)) { 216 | return $this->node->getAttribute($name); 217 | } 218 | 219 | return $default; 220 | } 221 | 222 | /** 223 | * Unset an attribute on the element. 224 | * 225 | * @param string $name The name of an attribute 226 | * 227 | * @return Element 228 | */ 229 | public function removeAttribute(string $name): self 230 | { 231 | $this->node->removeAttribute($name); 232 | 233 | return $this; 234 | } 235 | 236 | /** 237 | * Unset all attributes of the element. 238 | * 239 | * @param string[] $preserved 240 | * 241 | * @return Element 242 | */ 243 | public function removeAllAttributes(array $preserved = []): self 244 | { 245 | if ( ! $this->node instanceof DOMElement) { 246 | return $this; 247 | } 248 | 249 | foreach ($this->attributes() as $name => $value) { 250 | if (in_array($name, $preserved, true)) { 251 | continue; 252 | } 253 | 254 | $this->node->removeAttribute($name); 255 | } 256 | 257 | return $this; 258 | } 259 | 260 | /** 261 | * Alias for getAttribute and setAttribute methods. 262 | * 263 | * @param string $name The name of an attribute 264 | * @param string|null $value The value that will be returned an attribute doesn't exist 265 | * 266 | * @return string|null|Element 267 | */ 268 | public function attr(string $name, ?string $value = null) 269 | { 270 | if ($value === null) { 271 | return $this->getAttribute($name); 272 | } 273 | 274 | return $this->setAttribute($name, $value); 275 | } 276 | 277 | /** 278 | * Returns the node attributes or null, if it is not DOMElement. 279 | * 280 | * @param string[] $names 281 | * 282 | * @return array|null 283 | */ 284 | public function attributes(array $names = null): ?array 285 | { 286 | if ( ! $this->node instanceof DOMElement) { 287 | return null; 288 | } 289 | 290 | if ($names === null) { 291 | $result = []; 292 | 293 | foreach ($this->node->attributes as $name => $attribute) { 294 | $result[$name] = $attribute->value; 295 | } 296 | 297 | return $result; 298 | } 299 | 300 | $result = []; 301 | 302 | foreach ($this->node->attributes as $name => $attribute) { 303 | if (in_array($name, $names, true)) { 304 | $result[$name] = $attribute->value; 305 | } 306 | } 307 | 308 | return $result; 309 | } 310 | 311 | /** 312 | * @return ClassAttribute 313 | * 314 | * @throws LogicException if the node is not an instance of DOMElement 315 | */ 316 | public function classes(): ClassAttribute 317 | { 318 | if ($this->classAttribute !== null) { 319 | return $this->classAttribute; 320 | } 321 | 322 | if ( ! $this->isElementNode()) { 323 | throw new LogicException('Class attribute is available only for element nodes.'); 324 | } 325 | 326 | $this->classAttribute = new ClassAttribute($this); 327 | 328 | return $this->classAttribute; 329 | } 330 | 331 | /** 332 | * @return StyleAttribute 333 | * 334 | * @throws LogicException if the node is not an instance of DOMElement 335 | */ 336 | public function style(): StyleAttribute 337 | { 338 | if ($this->styleAttribute !== null) { 339 | return $this->styleAttribute; 340 | } 341 | 342 | if ( ! $this->isElementNode()) { 343 | throw new LogicException('Style attribute is available only for element nodes.'); 344 | } 345 | 346 | $this->styleAttribute = new StyleAttribute($this); 347 | 348 | return $this->styleAttribute; 349 | } 350 | 351 | /** 352 | * Dynamically set an attribute on the element. 353 | * 354 | * @param string $name The name of an attribute 355 | * @param string|integer|float $value The value of an attribute 356 | * 357 | * @return Element 358 | */ 359 | public function __set(string $name, $value) 360 | { 361 | return $this->setAttribute($name, $value); 362 | } 363 | 364 | /** 365 | * Dynamically access the element's attributes. 366 | * 367 | * @param string $name The name of an attribute 368 | * 369 | * @return string|null 370 | */ 371 | public function __get(string $name): ?string 372 | { 373 | return $this->getAttribute($name); 374 | } 375 | 376 | /** 377 | * Determine if an attribute exists on the element. 378 | * 379 | * @param string $name The attribute name 380 | * 381 | * @return bool 382 | */ 383 | public function __isset(string $name): bool 384 | { 385 | return $this->hasAttribute($name); 386 | } 387 | 388 | /** 389 | * Unset an attribute on the model. 390 | * 391 | * @param string $name The name of an attribute 392 | */ 393 | public function __unset(string $name) 394 | { 395 | $this->removeAttribute($name); 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/src/DiDom/Encoder.php: -------------------------------------------------------------------------------- 1 | $codes[$characterIndex]) { 43 | $entities .= chr($codes[$characterIndex++]); 44 | 45 | continue; 46 | } 47 | 48 | if (0xF0 <= $codes[$characterIndex]) { 49 | $code = (($codes[$characterIndex++] - 0xF0) << 18) + (($codes[$characterIndex++] - 0x80) << 12) + (($codes[$characterIndex++] - 0x80) << 6) + $codes[$characterIndex++] - 0x80; 50 | } elseif (0xE0 <= $codes[$characterIndex]) { 51 | $code = (($codes[$characterIndex++] - 0xE0) << 12) + (($codes[$characterIndex++] - 0x80) << 6) + $codes[$characterIndex++] - 0x80; 52 | } else { 53 | $code = (($codes[$characterIndex++] - 0xC0) << 6) + $codes[$characterIndex++] - 0x80; 54 | } 55 | 56 | $entities .= '&#' . $code . ';'; 57 | } 58 | 59 | return $entities; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/src/DiDom/Errors.php: -------------------------------------------------------------------------------- 1 | ') { 97 | $prefix = '/'; 98 | 99 | $selector = ltrim($selector, '> '); 100 | } 101 | 102 | $segments = self::getSegments($selector); 103 | $xpath = ''; 104 | 105 | while (count($segments) > 0) { 106 | $xpath .= self::buildXpath($segments, $prefix); 107 | 108 | $selector = trim(substr($selector, strlen($segments['selector']))); 109 | $prefix = isset($segments['rel']) ? '/' : '//'; 110 | 111 | if ($selector === '' || substr($selector, 0, 2) === '::' || substr($selector, 0, 1) === ',') { 112 | break; 113 | } 114 | 115 | $segments = self::getSegments($selector); 116 | } 117 | 118 | // if selector has property 119 | if (substr($selector, 0, 2) === '::') { 120 | $property = self::parseProperty($selector); 121 | $propertyXpath = self::convertProperty($property['name'], $property['args']); 122 | 123 | $selector = substr($selector, strlen($property['property'])); 124 | $selector = trim($selector); 125 | 126 | $xpath .= '/' . $propertyXpath; 127 | } 128 | 129 | return [$xpath, $selector]; 130 | } 131 | 132 | /** 133 | * @param string $selector 134 | * 135 | * @return array 136 | * 137 | * @throws InvalidSelectorException 138 | */ 139 | protected static function parseProperty(string $selector): array 140 | { 141 | $name = '(?P[\w\-]+)'; 142 | $args = '(?:\((?P[^\)]+)?\))?'; 143 | 144 | $regexp = '/^::' . $name . $args . '/is'; 145 | 146 | if (preg_match($regexp, $selector, $matches) !== 1) { 147 | throw new InvalidSelectorException(sprintf('Invalid property "%s".', $selector)); 148 | } 149 | 150 | $result = []; 151 | 152 | $result['property'] = $matches[0]; 153 | $result['name'] = $matches['name']; 154 | $result['args'] = isset($matches['args']) ? explode(',', $matches['args']) : []; 155 | 156 | $result['args'] = array_map('trim', $result['args']); 157 | 158 | return $result; 159 | } 160 | 161 | /** 162 | * @param string $name 163 | * @param array $parameters 164 | * 165 | * @return string 166 | * 167 | * @throws InvalidSelectorException if the specified property is unknown 168 | */ 169 | protected static function convertProperty(string $name, array $parameters = []): string 170 | { 171 | if ($name === 'text') { 172 | return 'text()'; 173 | } 174 | 175 | if ($name === 'attr') { 176 | if (count($parameters) === 0) { 177 | return '@*'; 178 | } 179 | 180 | $attributes = []; 181 | 182 | foreach ($parameters as $attribute) { 183 | $attributes[] = sprintf('name() = "%s"', $attribute); 184 | } 185 | 186 | return sprintf('@*[%s]', implode(' or ', $attributes)); 187 | } 188 | 189 | throw new InvalidSelectorException(sprintf('Unknown property "%s".', $name)); 190 | } 191 | 192 | /** 193 | * Converts a CSS pseudo-class into an XPath expression. 194 | * 195 | * @param string $pseudo Pseudo-class 196 | * @param string $tagName 197 | * @param array $parameters 198 | * 199 | * @return string 200 | * 201 | * @throws InvalidSelectorException if the specified pseudo-class is unknown 202 | */ 203 | protected static function convertPseudo(string $pseudo, string &$tagName, array $parameters = []): string 204 | { 205 | switch ($pseudo) { 206 | case 'first-child': 207 | return 'position() = 1'; 208 | case 'last-child': 209 | return 'position() = last()'; 210 | case 'nth-child': 211 | $xpath = sprintf('(name()="%s") and (%s)', $tagName, self::convertNthExpression($parameters[0])); 212 | $tagName = '*'; 213 | 214 | return $xpath; 215 | case 'contains': 216 | $string = trim($parameters[0], '\'"'); 217 | 218 | if (count($parameters) === 1) { 219 | return self::convertContains($string); 220 | } 221 | 222 | if ($parameters[1] !== 'true' && $parameters[1] !== 'false') { 223 | throw new InvalidSelectorException(sprintf('Parameter 2 of "contains" pseudo-class must be equal true or false, "%s" given.', $parameters[1])); 224 | } 225 | 226 | $caseSensitive = $parameters[1] === 'true'; 227 | 228 | if (count($parameters) === 2) { 229 | return self::convertContains($string, $caseSensitive); 230 | } 231 | 232 | if ($parameters[2] !== 'true' && $parameters[2] !== 'false') { 233 | throw new InvalidSelectorException(sprintf('Parameter 3 of "contains" pseudo-class must be equal true or false, "%s" given.', $parameters[2])); 234 | } 235 | 236 | $fullMatch = $parameters[2] === 'true'; 237 | 238 | return self::convertContains($string, $caseSensitive, $fullMatch); 239 | case 'has': 240 | return self::cssToXpath($parameters[0], './/'); 241 | case 'not': 242 | return sprintf('not(self::%s)', self::cssToXpath($parameters[0], '')); 243 | 244 | case 'nth-of-type': 245 | return self::convertNthExpression($parameters[0]); 246 | case 'empty': 247 | return 'count(descendant::*) = 0'; 248 | case 'not-empty': 249 | return 'count(descendant::*) > 0'; 250 | } 251 | 252 | throw new InvalidSelectorException(sprintf('Unknown pseudo-class "%s".', $pseudo)); 253 | } 254 | 255 | /** 256 | * @param array $segments 257 | * @param string $prefix Specifies the nesting of nodes 258 | * 259 | * @return string XPath expression 260 | * 261 | * @throws InvalidArgumentException if you neither specify tag name nor attributes 262 | */ 263 | public static function buildXpath(array $segments, string $prefix = '//'): string 264 | { 265 | $tagName = isset($segments['tag']) ? $segments['tag'] : '*'; 266 | 267 | $attributes = []; 268 | 269 | // if the id attribute specified 270 | if (isset($segments['id'])) { 271 | $attributes[] = sprintf('@id="%s"', $segments['id']); 272 | } 273 | 274 | // if the class attribute specified 275 | if (isset($segments['classes'])) { 276 | foreach ($segments['classes'] as $class) { 277 | $attributes[] = sprintf('contains(concat(" ", normalize-space(@class), " "), " %s ")', $class); 278 | } 279 | } 280 | 281 | // if the attributes specified 282 | if (isset($segments['attributes'])) { 283 | foreach ($segments['attributes'] as $name => $value) { 284 | $attributes[] = self::convertAttribute($name, $value); 285 | } 286 | } 287 | 288 | // if the pseudo class specified 289 | if (array_key_exists('pseudo', $segments)) { 290 | foreach ($segments['pseudo'] as $pseudo) { 291 | $expression = $pseudo['expression'] !== null ? $pseudo['expression'] : ''; 292 | 293 | $parameters = explode(',', $expression); 294 | $parameters = array_map('trim', $parameters); 295 | 296 | $attributes[] = self::convertPseudo($pseudo['type'], $tagName, $parameters); 297 | } 298 | } 299 | 300 | if (count($attributes) === 0 && ! isset($segments['tag'])) { 301 | throw new InvalidArgumentException('The array of segments must contain the name of the tag or at least one attribute.'); 302 | } 303 | 304 | $xpath = $prefix . $tagName; 305 | 306 | if ($count = count($attributes)) { 307 | $xpath .= ($count > 1) ? sprintf('[(%s)]', implode(') and (', $attributes)) : sprintf('[%s]', $attributes[0]); 308 | } 309 | 310 | return $xpath; 311 | } 312 | 313 | /** 314 | * @param string $name The name of an attribute 315 | * @param string|null $value The value of an attribute 316 | * 317 | * @return string 318 | */ 319 | protected static function convertAttribute(string $name, ?string $value): string 320 | { 321 | $isSimpleSelector = ! in_array(substr($name, 0, 1), ['^', '!'], true); 322 | $isSimpleSelector = $isSimpleSelector && ( ! in_array(substr($name, -1), ['^', '$', '*', '!', '~'], true)); 323 | 324 | if ($isSimpleSelector) { 325 | // if specified only the attribute name 326 | $xpath = $value === null ? '@' . $name : sprintf('@%s="%s"', $name, $value); 327 | 328 | return $xpath; 329 | } 330 | 331 | // if the attribute name starts with ^ 332 | // example: *[^data-] 333 | if (substr($name, 0, 1) === '^') { 334 | $xpath = sprintf('@*[starts-with(name(), "%s")]', substr($name, 1)); 335 | 336 | return $value === null ? $xpath : sprintf('%s="%s"', $xpath, $value); 337 | } 338 | 339 | // if the attribute name starts with ! 340 | // example: input[!disabled] 341 | if (substr($name, 0, 1) === '!') { 342 | $xpath = sprintf('not(@%s)', substr($name, 1)); 343 | 344 | return $xpath; 345 | } 346 | 347 | $symbol = substr($name, -1); 348 | $name = substr($name, 0, -1); 349 | 350 | switch ($symbol) { 351 | case '^': 352 | $xpath = sprintf('starts-with(@%s, "%s")', $name, $value); 353 | 354 | break; 355 | case '$': 356 | $xpath = sprintf('substring(@%s, string-length(@%s) - string-length("%s") + 1) = "%s"', $name, $name, $value, $value); 357 | 358 | break; 359 | case '*': 360 | $xpath = sprintf('contains(@%s, "%s")', $name, $value); 361 | 362 | break; 363 | case '!': 364 | $xpath = sprintf('not(@%s="%s")', $name, $value); 365 | 366 | break; 367 | case '~': 368 | $xpath = sprintf('contains(concat(" ", normalize-space(@%s), " "), " %s ")', $name, $value); 369 | 370 | break; 371 | } 372 | 373 | return $xpath; 374 | } 375 | 376 | /** 377 | * Converts nth-expression into an XPath expression. 378 | * 379 | * @param string $expression nth-expression 380 | * 381 | * @return string 382 | * 383 | * @throws InvalidSelectorException if the given nth-child expression is empty or invalid 384 | */ 385 | protected static function convertNthExpression(string $expression): string 386 | { 387 | if ($expression === '') { 388 | throw new InvalidSelectorException('nth-child (or nth-last-child) expression must not be empty.'); 389 | } 390 | 391 | if ($expression === 'odd') { 392 | return 'position() mod 2 = 1 and position() >= 1'; 393 | } 394 | 395 | if ($expression === 'even') { 396 | return 'position() mod 2 = 0 and position() >= 0'; 397 | } 398 | 399 | if (is_numeric($expression)) { 400 | return sprintf('position() = %d', $expression); 401 | } 402 | 403 | if (preg_match("/^(?P[0-9]?n)(?:(?P\+|\-)(?P[0-9]+))?$/is", $expression, $segments)) { 404 | if (isset($segments['mul'])) { 405 | $multiplier = $segments['mul'] === 'n' ? 1 : trim($segments['mul'], 'n'); 406 | $sign = (isset($segments['sign']) && $segments['sign'] === '+') ? '-' : '+'; 407 | $position = isset($segments['pos']) ? $segments['pos'] : 0; 408 | 409 | return sprintf('(position() %s %d) mod %d = 0 and position() >= %d', $sign, $position, $multiplier, $position); 410 | } 411 | } 412 | 413 | throw new InvalidSelectorException(sprintf('Invalid nth-child expression "%s".', $expression)); 414 | } 415 | 416 | /** 417 | * @param string $string 418 | * @param bool $caseSensitive 419 | * @param bool $fullMatch 420 | * 421 | * @return string 422 | */ 423 | protected static function convertContains(string $string, bool $caseSensitive = true, bool $fullMatch = false): string 424 | { 425 | if ($caseSensitive && $fullMatch) { 426 | return sprintf('text() = "%s"', $string); 427 | } 428 | 429 | if ($caseSensitive && ! $fullMatch) { 430 | return sprintf('contains(text(), "%s")', $string); 431 | } 432 | 433 | $strToLowerFunction = function_exists('mb_strtolower') ? 'mb_strtolower' : 'strtolower'; 434 | 435 | if ( ! $caseSensitive && $fullMatch) { 436 | return sprintf("php:functionString(\"{$strToLowerFunction}\", .) = php:functionString(\"{$strToLowerFunction}\", \"%s\")", $string); 437 | } 438 | 439 | // if ! $caseSensitive and ! $fullMatch 440 | return sprintf("contains(php:functionString(\"{$strToLowerFunction}\", .), php:functionString(\"{$strToLowerFunction}\", \"%s\"))", $string); 441 | } 442 | 443 | /** 444 | * Splits the CSS selector into parts (tag name, ID, classes, attributes, pseudo-class). 445 | * 446 | * @param string $selector CSS selector 447 | * 448 | * @return array 449 | * 450 | * @throws InvalidSelectorException if the selector is empty or not valid 451 | */ 452 | public static function getSegments(string $selector): array 453 | { 454 | $selector = trim($selector); 455 | 456 | if ($selector === '') { 457 | throw new InvalidSelectorException('The selector must not be empty.'); 458 | } 459 | 460 | $pregMatchResult = preg_match(self::getSelectorRegex(), $selector, $segments); 461 | 462 | if ($pregMatchResult === false || $pregMatchResult === 0 || $segments[0] === '') { 463 | throw new InvalidSelectorException(sprintf('Invalid selector "%s".', $selector)); 464 | } 465 | 466 | $result = ['selector' => $segments[0]]; 467 | 468 | if (isset($segments['tag']) && $segments['tag'] !== '') { 469 | $result['tag'] = $segments['tag']; 470 | } 471 | 472 | // if the id attribute specified 473 | if (isset($segments['id']) && $segments['id'] !== '') { 474 | $result['id'] = $segments['id']; 475 | } 476 | 477 | // if the attributes specified 478 | if (isset($segments['attrs'])) { 479 | $attributes = trim($segments['attrs'], '[]'); 480 | $attributes = explode('][', $attributes); 481 | 482 | foreach ($attributes as $attribute) { 483 | if ($attribute !== '') { 484 | list($name, $value) = array_pad(explode('=', $attribute, 2), 2, null); 485 | 486 | if ($name === '') { 487 | throw new InvalidSelectorException(sprintf('Invalid selector "%s": attribute name must not be empty.', $selector)); 488 | } 489 | 490 | // equal null if specified only the attribute name 491 | $result['attributes'][$name] = is_string($value) ? trim($value, '\'"') : null; 492 | } 493 | } 494 | } 495 | 496 | // if the class attribute specified 497 | if (isset($segments['classes'])) { 498 | $classes = trim($segments['classes'], '.'); 499 | $classes = explode('.', $classes); 500 | 501 | foreach ($classes as $class) { 502 | if ($class !== '') { 503 | $result['classes'][] = $class; 504 | } 505 | } 506 | } 507 | 508 | // if the pseudo class specified 509 | if (isset($segments['pseudo']) && $segments['pseudo'] !== '') { 510 | preg_match_all('/:(?P[\w\-]+)(?:\((?P[^\)]+)\))?/', $segments['pseudo'], $pseudoClasses); 511 | 512 | $result['pseudo'] = []; 513 | 514 | foreach ($pseudoClasses['type'] as $index => $pseudoType) { 515 | $result['pseudo'][] = [ 516 | 'type' => $pseudoType, 517 | 'expression' => $pseudoClasses['expr'][$index] !== '' ? $pseudoClasses['expr'][$index] : null, 518 | ]; 519 | } 520 | } 521 | 522 | // if it is a direct descendant 523 | if (isset($segments['rel'])) { 524 | $result['rel'] = $segments['rel']; 525 | } 526 | 527 | return $result; 528 | } 529 | 530 | private static function getSelectorRegex(): string 531 | { 532 | $tag = '(?P[\*|\w|\-]+)?'; 533 | $id = '(?:#(?P[\w|\-]+))?'; 534 | $classes = '(?P\.[\w|\-|\.]+)*'; 535 | $attrs = '(?P(?:\[.+?\])*)?'; 536 | $pseudoType = '[\w\-]+'; 537 | $pseudoExpr = '(?:\([^\)]+\))?'; 538 | $pseudo = '(?P(?::' . $pseudoType . $pseudoExpr . ')+)?'; 539 | $rel = '\s*(?P>)?'; 540 | 541 | return '/' . $tag . $id . $classes . $attrs . $pseudo . $rel . '/is'; 542 | } 543 | 544 | /** 545 | * @return array 546 | */ 547 | public static function getCompiled(): array 548 | { 549 | return static::$compiled; 550 | } 551 | 552 | /** 553 | * @param array $compiled 554 | * 555 | * @throws InvalidArgumentException if the attributes is not an array 556 | */ 557 | public static function setCompiled(array $compiled): void 558 | { 559 | static::$compiled = $compiled; 560 | } 561 | } 562 | -------------------------------------------------------------------------------- /vendor/imangazaliev/didom/src/DiDom/StyleAttribute.php: -------------------------------------------------------------------------------- 1 | isElementNode()) { 36 | throw new InvalidArgumentException(sprintf('The element must contain DOMElement node.')); 37 | } 38 | 39 | $this->element = $element; 40 | 41 | $this->parseStyleAttribute(); 42 | } 43 | 44 | /** 45 | * Parses style attribute of the element. 46 | */ 47 | protected function parseStyleAttribute() 48 | { 49 | if ( ! $this->element->hasAttribute('style')) { 50 | // possible if style attribute has been removed 51 | if ($this->styleString !== '') { 52 | $this->styleString = ''; 53 | $this->properties = []; 54 | } 55 | 56 | return; 57 | } 58 | 59 | // if style attribute is not changed 60 | if ($this->element->getAttribute('style') === $this->styleString) { 61 | return; 62 | } 63 | 64 | // save style attribute as is (without trimming) 65 | $this->styleString = $this->element->getAttribute('style'); 66 | 67 | $styleString = trim($this->styleString, ' ;'); 68 | 69 | if ($styleString === '') { 70 | $this->properties = []; 71 | 72 | return; 73 | } 74 | 75 | $properties = explode(';', $styleString); 76 | 77 | foreach ($properties as $property) { 78 | list($name, $value) = explode(':', $property, 2); 79 | 80 | $name = trim($name); 81 | $value = trim($value); 82 | 83 | $this->properties[$name] = $value; 84 | } 85 | } 86 | 87 | /** 88 | * Updates style attribute of the element. 89 | */ 90 | protected function updateStyleAttribute(): void 91 | { 92 | $this->styleString = $this->buildStyleString(); 93 | 94 | $this->element->setAttribute('style', $this->styleString); 95 | } 96 | 97 | /** 98 | * @return string 99 | */ 100 | protected function buildStyleString(): string 101 | { 102 | $properties = []; 103 | 104 | foreach ($this->properties as $propertyName => $value) { 105 | $properties[] = $propertyName . ': ' . $value; 106 | } 107 | 108 | return implode('; ', $properties); 109 | } 110 | 111 | /** 112 | * @param string $name 113 | * @param string $value 114 | * 115 | * @return StyleAttribute 116 | * 117 | * @throws InvalidArgumentException if property name is not a string 118 | * @throws InvalidArgumentException if property value is not a string 119 | */ 120 | public function setProperty(string $name, string $value): self 121 | { 122 | $this->parseStyleAttribute(); 123 | 124 | $this->properties[$name] = $value; 125 | 126 | $this->updateStyleAttribute(); 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * @param array $properties 133 | * 134 | * @return StyleAttribute 135 | * 136 | * @throws InvalidArgumentException if property name is not a string 137 | * @throws InvalidArgumentException if property value is not a string 138 | */ 139 | public function setMultipleProperties(array $properties): self 140 | { 141 | $this->parseStyleAttribute(); 142 | 143 | foreach ($properties as $propertyName => $value) { 144 | if ( ! is_string($propertyName)) { 145 | throw new InvalidArgumentException(sprintf('Property name must be a string, %s given.', (is_object($propertyName) ? get_class($propertyName) : gettype($propertyName)))); 146 | } 147 | 148 | if ( ! is_string($value)) { 149 | throw new InvalidArgumentException(sprintf('Property value must be a string, %s given.', (is_object($value) ? get_class($value) : gettype($value)))); 150 | } 151 | 152 | $this->properties[$propertyName] = $value; 153 | } 154 | 155 | $this->updateStyleAttribute(); 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * @param string $name 162 | * @param mixed $default 163 | * 164 | * @return mixed 165 | */ 166 | public function getProperty(string $name, $default = null) 167 | { 168 | $this->parseStyleAttribute(); 169 | 170 | if ( ! array_key_exists($name, $this->properties)) { 171 | return $default; 172 | } 173 | 174 | return $this->properties[$name]; 175 | } 176 | 177 | /** 178 | * @param string[] $propertyNames 179 | * 180 | * @return array 181 | * 182 | * @throws InvalidArgumentException if property name is not a string 183 | */ 184 | public function getMultipleProperties(array $propertyNames): array 185 | { 186 | $this->parseStyleAttribute(); 187 | 188 | $result = []; 189 | 190 | foreach ($propertyNames as $propertyName) { 191 | if ( ! is_string($propertyName)) { 192 | throw new InvalidArgumentException(sprintf('Property name must be a string, %s given.', (is_object($propertyName) ? get_class($propertyName) : gettype($propertyName)))); 193 | } 194 | 195 | if (array_key_exists($propertyName, $this->properties)) { 196 | $result[$propertyName] = $this->properties[$propertyName]; 197 | } 198 | } 199 | 200 | return $result; 201 | } 202 | 203 | /** 204 | * @return array 205 | */ 206 | public function getAllProperties(): array 207 | { 208 | $this->parseStyleAttribute(); 209 | 210 | return $this->properties; 211 | } 212 | 213 | /** 214 | * @param string $name 215 | * 216 | * @return bool 217 | */ 218 | public function hasProperty(string $name): bool 219 | { 220 | $this->parseStyleAttribute(); 221 | 222 | return array_key_exists($name, $this->properties); 223 | } 224 | 225 | /** 226 | * @param string $name 227 | * 228 | * @return StyleAttribute 229 | * 230 | * @throws InvalidArgumentException if property name is not a string 231 | */ 232 | public function removeProperty(string $name): self 233 | { 234 | $this->parseStyleAttribute(); 235 | 236 | unset($this->properties[$name]); 237 | 238 | $this->updateStyleAttribute(); 239 | 240 | return $this; 241 | } 242 | 243 | /** 244 | * @param array $propertyNames 245 | * 246 | * @return StyleAttribute 247 | * 248 | * @throws InvalidArgumentException if property name is not a string 249 | */ 250 | public function removeMultipleProperties(array $propertyNames): self 251 | { 252 | $this->parseStyleAttribute(); 253 | 254 | foreach ($propertyNames as $propertyName) { 255 | if ( ! is_string($propertyName)) { 256 | throw new InvalidArgumentException(sprintf('Property name must be a string, %s given.', (is_object($propertyName) ? get_class($propertyName) : gettype($propertyName)))); 257 | } 258 | 259 | unset($this->properties[$propertyName]); 260 | } 261 | 262 | $this->updateStyleAttribute(); 263 | 264 | return $this; 265 | } 266 | 267 | /** 268 | * @param string[] $preserved 269 | * 270 | * @return StyleAttribute 271 | */ 272 | public function removeAllProperties(array $preserved = []): self 273 | { 274 | $this->parseStyleAttribute(); 275 | 276 | $preservedProperties = []; 277 | 278 | foreach ($preserved as $propertyName) { 279 | if ( ! is_string($propertyName)) { 280 | throw new InvalidArgumentException(sprintf('Property name must be a string, %s given.', (is_object($propertyName) ? get_class($propertyName) : gettype($propertyName)))); 281 | } 282 | 283 | if ( ! array_key_exists($propertyName, $this->properties)) { 284 | continue; 285 | } 286 | 287 | $preservedProperties[$propertyName] = $this->properties[$propertyName]; 288 | } 289 | 290 | $this->properties = $preservedProperties; 291 | 292 | $this->updateStyleAttribute(); 293 | 294 | return $this; 295 | } 296 | 297 | /** 298 | * @return Element 299 | */ 300 | public function getElement(): Element 301 | { 302 | return $this->element; 303 | } 304 | } 305 | --------------------------------------------------------------------------------