├── .github ├── CODEOWNERS ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── stale.yml ├── CONTRIBUTING.md ├── settings.yml ├── workflows │ └── continuous-integration.yml └── CODE_OF_CONDUCT.md ├── src ├── Tag │ ├── Tag.php │ ├── Comment.php │ ├── TagFactoryInterface.php │ ├── TagInterface.php │ ├── TagFactory.php │ └── AbstractTag.php ├── Attribute │ ├── Attribute.php │ ├── AttributeFactoryInterface.php │ ├── AttributeFactory.php │ ├── AttributeInterface.php │ └── AbstractAttribute.php ├── Attributes │ ├── Attributes.php │ ├── AttributesFactoryInterface.php │ ├── AttributesFactory.php │ ├── AttributesInterface.php │ └── AbstractAttributes.php ├── RenderableInterface.php ├── StringableInterface.php ├── AlterableInterface.php ├── EscapableInterface.php ├── PreprocessableInterface.php ├── HtmlTag.php ├── HtmlTagInterface.php ├── HtmlBuilder.php └── AbstractBaseHtmlTagObject.php ├── phpbench.json.dist ├── psalm.xml ├── benchmarks ├── AttributeBench.php ├── AttributesBench.php ├── AbstractBench.php └── TagBench.php ├── LICENSE ├── composer.json └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @drupol 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: drupol 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This PR 2 | 3 | * [x] 4 | * [ ] 5 | * [ ] 6 | 7 | Follows #. 8 | Related to #. 9 | Fixes #. 10 | -------------------------------------------------------------------------------- /src/Tag/Tag.php: -------------------------------------------------------------------------------- 1 | ', $this->renderContent()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 60 2 | 3 | daysUntilClose: 7 4 | 5 | staleLabel: stale 6 | 7 | markComment: > 8 | This issue has been automatically marked as stale because it has not had 9 | recent activity. It will be closed if no further activity occurs. Thank you 10 | for your contributions. 11 | -------------------------------------------------------------------------------- /src/RenderableInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/PreprocessableInterface.php: -------------------------------------------------------------------------------- 1 | $values 16 | * The values to preprocess. 17 | * @param array $context 18 | * The context. 19 | * 20 | * @return array 21 | * The values. 22 | */ 23 | public function preprocess(array $values, array $context = []): array; 24 | } 25 | -------------------------------------------------------------------------------- /benchmarks/AttributeBench.php: -------------------------------------------------------------------------------- 1 | getAttributes() as $name => $value) { 22 | $attribute = new Attribute($name, $value); 23 | $attribute->render(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | We're using [Travis CI](https://travis-ci.com) as a continuous integration system. 4 | 5 | For details, see [`.travis.yml`](../.travis.yml). 6 | 7 | ## Tests 8 | 9 | We're using [`grumphp/grumphp`](https://github.com/phpro/grumphp) to drive the development. 10 | 11 | Run 12 | 13 | ```bash 14 | ./vendor/bin/grumphp run 15 | ``` 16 | 17 | to run all the tests. 18 | 19 | ## Coding Standards 20 | 21 | We are using [`drupol/php-conventions`](https://github.com/drupol/php-conventions) to enforce coding standards. 22 | 23 | Run 24 | 25 | ```bash 26 | ./vendor/bin/grumphp run 27 | ``` 28 | 29 | to automatically detect/fix coding standard violations. 30 | -------------------------------------------------------------------------------- /src/Attributes/AttributesFactoryInterface.php: -------------------------------------------------------------------------------- 1 | $attributes 15 | * The tag attributes 16 | * @param mixed $content 17 | * The tag content 18 | * 19 | * @return \drupol\htmltag\Tag\TagInterface 20 | * The tag 21 | */ 22 | public static function build(string $name, array $attributes = [], $content = null): TagInterface; 23 | 24 | /** 25 | * Create a new tag. 26 | * 27 | * @param string $name 28 | * The tag name 29 | * @param array $attributes 30 | * The tag attributes 31 | * @param mixed $content 32 | * The tag content 33 | * 34 | * @return \drupol\htmltag\Tag\TagInterface 35 | * The tag 36 | */ 37 | public function getInstance(string $name, array $attributes = [], $content = null): TagInterface; 38 | } 39 | -------------------------------------------------------------------------------- /benchmarks/AttributesBench.php: -------------------------------------------------------------------------------- 1 | attributeFactory, $this->getAttributes()); 30 | $attributes->render(); 31 | } 32 | 33 | /** 34 | * Init the attribute factory object. 35 | */ 36 | public function initAttributesRender() 37 | { 38 | $this->attributeFactory = new AttributeFactory(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pol Dellaiera 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 | -------------------------------------------------------------------------------- /benchmarks/AbstractBench.php: -------------------------------------------------------------------------------- 1 | true, 22 | 'bool-false' => false, 23 | 'bool-true-array' => array_fill(0, 3, true), 24 | 'bool-false-array' => array_fill(0, 3, false), 25 | 'integer' => 0, 26 | 'integer-array' => range(0, 5), 27 | 'integer-nested-array' => [0, [1, [2, [3, [4, [5]]]]]], 28 | 'float' => M_PI, 29 | 'float-array' => [M_1_PI, M_2_PI, M_PI, M_PI_2, M_PI_4], 30 | 'float-nested-array' => [ 31 | M_1_PI, 32 | [M_2_PI, [M_PI, [M_PI_2, [M_PI_4]]]], 33 | ], 34 | 'string' => ' a b c d e f ', 35 | 'string-array' => range('a', 'f'), 36 | 'string-array-spaces' => ['a ', ' b ', ' c ', ' d ', ' e'], 37 | 'string-nested-array' => ['a', ['b', ['c', ['d', ['e', ['f']]]]]], 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/probot/settings 2 | 3 | branches: 4 | - name: master 5 | protection: 6 | enforce_admins: false 7 | required_pull_request_reviews: 8 | dismiss_stale_reviews: true 9 | require_code_owner_reviews: true 10 | required_approving_review_count: 1 11 | required_status_checks: 12 | contexts: 13 | - "Grumphp" 14 | strict: false 15 | restrictions: null 16 | 17 | labels: 18 | - name: bug 19 | color: ee0701 20 | 21 | - name: dependencies 22 | color: 0366d6 23 | 24 | - name: enhancement 25 | color: 0e8a16 26 | 27 | - name: question 28 | color: cc317c 29 | 30 | - name: security 31 | color: ee0701 32 | 33 | - name: stale 34 | color: eeeeee 35 | 36 | repository: 37 | allow_merge_commit: true 38 | allow_rebase_merge: false 39 | allow_squash_merge: false 40 | default_branch: master 41 | description: "A fast and extensible helper library for generating HTML, create tags, their attributes and content." 42 | topics: html, tags, attributes, html generation 43 | has_downloads: true 44 | has_issues: true 45 | has_pages: false 46 | has_projects: false 47 | has_wiki: false 48 | name: htmltag 49 | private: false 50 | -------------------------------------------------------------------------------- /src/Attributes/AttributesFactory.php: -------------------------------------------------------------------------------- 1 | AttributeFactory::class, 19 | '*' => Attributes::class, 20 | ]; 21 | 22 | public static function build( 23 | array $attributes = [] 24 | ) { 25 | return (new static())->getInstance($attributes); 26 | } 27 | 28 | public function getInstance( 29 | array $attributes = [] 30 | ) { 31 | $attribute_factory_classname = static::$registry['attribute_factory']; 32 | 33 | /** @var \drupol\htmltag\Attribute\AttributeFactoryInterface $attribute_factory_classname */ 34 | $attribute_factory_classname = (new ReflectionClass($attribute_factory_classname))->newInstance(); 35 | 36 | $attributes_classname = static::$registry['*']; 37 | 38 | /** @var \drupol\htmltag\Attributes\AttributesInterface $attributes */ 39 | $attributes = (new ReflectionClass($attributes_classname)) 40 | ->newInstanceArgs([ 41 | $attribute_factory_classname, 42 | $attributes, 43 | ]); 44 | 45 | return $attributes; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Tag/TagInterface.php: -------------------------------------------------------------------------------- 1 | 50 | * The content as an array 51 | */ 52 | public function getContentAsArray(): array; 53 | } 54 | -------------------------------------------------------------------------------- /src/HtmlTagInterface.php: -------------------------------------------------------------------------------- 1 | |string $value 22 | * The attribute value 23 | * 24 | * @return \drupol\htmltag\Attribute\AttributeInterface 25 | * The attribute 26 | */ 27 | public static function attribute(string $name, $value): AttributeInterface; 28 | 29 | /** 30 | * Create a new attributes. 31 | * 32 | * @param array $attributes 33 | * The attributes 34 | * 35 | * @return \drupol\htmltag\Attributes\AttributesInterface 36 | * The attributes 37 | */ 38 | public static function attributes(array $attributes = []): AttributesInterface; 39 | 40 | /** 41 | * Create a new tag. 42 | * 43 | * @param string $name 44 | * The tag name 45 | * @param array $attributes 46 | * The attributes 47 | * @param mixed $content 48 | * The content 49 | * 50 | * @return \drupol\htmltag\Tag\TagInterface 51 | * The tag 52 | */ 53 | public static function tag(string $name, array $attributes = [], $content = null): TagInterface; 54 | } 55 | -------------------------------------------------------------------------------- /src/Attribute/AttributeFactory.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static $registry = [ 20 | '*' => Attribute::class, 21 | ]; 22 | 23 | public static function build(string $name, $value = null): AttributeInterface 24 | { 25 | return (new static())->getInstance($name, $value); 26 | } 27 | 28 | public function getInstance(string $name, $value = null): AttributeInterface 29 | { 30 | $attribute_classname = static::$registry[$name] ?? static::$registry['*']; 31 | 32 | if (!in_array(AttributeInterface::class, class_implements($attribute_classname), true)) { 33 | throw new Exception( 34 | sprintf( 35 | 'The class (%s) must implement the interface %s.', 36 | $attribute_classname, 37 | AttributeInterface::class 38 | ) 39 | ); 40 | } 41 | 42 | /** @var \drupol\htmltag\Attribute\AttributeInterface $attribute */ 43 | $attribute = (new ReflectionClass($attribute_classname)) 44 | ->newInstanceArgs([ 45 | $name, 46 | $value, 47 | ]); 48 | 49 | return $attribute; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupol/htmltag", 3 | "type": "library", 4 | "description": "A fast and extensible helper library for generating HTML, create tags, their attributes and content.", 5 | "keywords": [ 6 | "html", 7 | "tags", 8 | "attributes", 9 | "html generation" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Pol Dellaiera", 15 | "email": "pol.dellaiera@protonmail.com" 16 | } 17 | ], 18 | "require": { 19 | "php": ">= 7.1.3" 20 | }, 21 | "require-dev": { 22 | "drupol/php-conventions": "^1.7.5 || ^1.8.19 || ^2.0.0", 23 | "friends-of-phpspec/phpspec-code-coverage": "^4", 24 | "infection/infection": "^0.13.6", 25 | "phpbench/phpbench": "^0.16", 26 | "phpspec/phpspec": "^5.1.2 || ^6.1", 27 | "phptaskman/changelog": "^1.0", 28 | "vimeo/psalm": "^3.17" 29 | }, 30 | "config": { 31 | "sort-packages": true 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "drupol\\htmltag\\": "./src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "drupol\\htmltag\\benchmarks\\": "./benchmarks/" 41 | }, 42 | "classmap": [ 43 | "spec/src/" 44 | ] 45 | }, 46 | "scripts": { 47 | "bench": "./vendor/bin/phpbench run --report=aggregate --store --precision=3", 48 | "grumphp": "./vendor/bin/grumphp run", 49 | "infection": "./vendor/bin/infection run -j 10", 50 | "phpcbf": "./vendor/bin/phpcbf --ignore=vendor .", 51 | "phpcs": "./vendor/bin/phpcs --ignore=vendor .", 52 | "phpspec": "./vendor/bin/phpspec run", 53 | "scrutinizer": "./vendor/bin/ocular code-coverage:upload --format=php-clover build/logs/clover.xml" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Tag/TagFactory.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public static $registry = [ 22 | 'attributes_factory' => AttributesFactory::class, 23 | '!--' => Comment::class, 24 | '*' => Tag::class, 25 | ]; 26 | 27 | public static function build( 28 | string $name, 29 | array $attributes = [], 30 | $content = null 31 | ): TagInterface { 32 | return (new static())->getInstance($name, $attributes, $content); 33 | } 34 | 35 | public function getInstance( 36 | string $name, 37 | array $attributes = [], 38 | $content = null 39 | ): TagInterface { 40 | $attributes_factory_classname = static::$registry['attributes_factory']; 41 | 42 | /** @var AttributesInterface $attributes */ 43 | $attributes = $attributes_factory_classname::build($attributes); 44 | 45 | $tag_classname = static::$registry[$name] ?? static::$registry['*']; 46 | 47 | if (!in_array(TagInterface::class, class_implements($tag_classname), true)) { 48 | throw new Exception( 49 | sprintf( 50 | 'The class (%s) must implement the interface %s.', 51 | $tag_classname, 52 | TagInterface::class 53 | ) 54 | ); 55 | } 56 | 57 | /** @var \drupol\htmltag\Tag\TagInterface $tag */ 58 | $tag = (new ReflectionClass($tag_classname)) 59 | ->newInstanceArgs([ 60 | $attributes, 61 | $name, 62 | $content, 63 | ]); 64 | 65 | return $tag; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/HtmlBuilder.php: -------------------------------------------------------------------------------- 1 | update( 37 | HtmlTag::tag('!--', [], $arguments[0]) 38 | ); 39 | } 40 | 41 | if ('_' === $name) { 42 | $this->scope = null; 43 | 44 | return $this; 45 | } 46 | 47 | $tag = TagFactory::build($name, ...$arguments); 48 | 49 | return $this->update($tag, true); 50 | } 51 | 52 | public function __toString(): string 53 | { 54 | $output = ''; 55 | 56 | foreach ($this->storage as $item) { 57 | $output .= $item; 58 | } 59 | 60 | return $output; 61 | } 62 | 63 | /** 64 | * Add the current tag to the stack. 65 | * 66 | * @param \drupol\htmltag\Tag\TagInterface $tag 67 | * The tag 68 | * @param bool $updateScope 69 | * True if the current scope needs to be updated 70 | * 71 | * @return \drupol\htmltag\HtmlBuilder 72 | * The HTML Builder object 73 | */ 74 | private function update(TagInterface $tag, $updateScope = false) 75 | { 76 | if (null !== $this->scope) { 77 | $this->scope->content($this->scope->getContentAsArray(), $tag); 78 | } else { 79 | $this->storage[] = $tag; 80 | } 81 | 82 | if (true === $updateScope) { 83 | $this->scope = $tag; 84 | } 85 | 86 | return $this; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /benchmarks/TagBench.php: -------------------------------------------------------------------------------- 1 | 'en']); 22 | 23 | $head = HtmlTag::tag('head'); 24 | 25 | $title = HtmlTag::tag('title'); 26 | $title->content(['Welcome to HTMLTag']); 27 | 28 | $meta1 = HtmlTag::tag('meta'); 29 | $meta1->attr('name')->set('author'); 30 | $meta1->attr('content')->set('Pol'); 31 | 32 | $meta2 = HtmlTag::tag('meta'); 33 | $meta2->attr('charset')->set('utf-8'); 34 | 35 | $head->content($title, $meta1, $meta2); 36 | 37 | $body = HtmlTag::tag('body'); 38 | $body->attr('class', 'logged-in', 'env-production', 'no-sidebar'); 39 | 40 | $header = HtmlTag::tag('header'); 41 | $header->attr('class', 'header f5'); 42 | $header->attr('role', 'banner'); 43 | $header->content(''); 44 | 45 | $menu = HtmlTag::tag('ul'); 46 | $menu->attr('class', ['user-nav d-flex flex-items-center', ['list-style-none']]); 47 | 48 | $menu_item1 = HtmlTag::tag('li'); 49 | $menu_item1->attr('class', 'active', 'dropdown'); 50 | $menu_item1->content('Home'); 51 | 52 | $menu_item2 = HtmlTag::tag('li'); 53 | $menu_item2 54 | ->attr('class', ['random', 'name class']); 55 | $menu_item2->content('Contact'); 56 | 57 | $menu->content($menu_item1, $menu_item2); 58 | 59 | $title = HtmlTag::tag('h1'); 60 | $title->attr('class')->set('title'); 61 | $title->content(['Welcome to HTMLTag']); 62 | 63 | $paragraph = HtmlTag::tag('p'); 64 | $paragraph->attr('class', 'section')->append('desc'); 65 | $paragraph->content(['This library helps you create HTML.']); 66 | $paragraph->attr('class')->remove('section')->replace('desc', 'description'); 67 | 68 | $content = HtmlTag::tag('div'); 69 | $content->attr('class', 'main-content content', 'main', 'content'); 70 | $content->content('Hello world!', $paragraph); 71 | 72 | $body->content($header, $title, $content); 73 | 74 | $html->content($head, $body); 75 | 76 | $html->render(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/categories/automating-your-workflow-with-github-actions 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | name: "Continuous Integration" 8 | 9 | jobs: 10 | run: 11 | name: "Grumphp" 12 | runs-on: ${{ matrix.operating-system }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | operating-system: [ubuntu-latest, windows-latest, macOS-latest] 17 | php-versions: ['7.1', '7.2', '7.3', '7.4'] 18 | 19 | steps: 20 | - name: Set git to use LF 21 | run: | 22 | git config --global core.autocrlf false 23 | git config --global core.eol lf 24 | 25 | - name: Checkout 26 | uses: actions/checkout@v2.3.3 27 | with: 28 | fetch-depth: 1 29 | 30 | - name: Install PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.php-versions }} 34 | extensions: gd,mbstring,xdebug 35 | 36 | - name: Get Composer Cache Directory 37 | id: composer-cache 38 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 39 | 40 | - name: Cache dependencies 41 | uses: actions/cache@v2 42 | with: 43 | path: ${{ steps.composer-cache.outputs.dir }} 44 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 45 | restore-keys: ${{ runner.os }}-composer- 46 | 47 | - name: Install dependencies 48 | run: composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader 49 | 50 | - name: Run Grumphp 51 | run: vendor/bin/grumphp run --no-ansi -n 52 | env: 53 | STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} 54 | 55 | - name: Send PSALM data 56 | run: vendor/bin/psalm --shepherd --stats 57 | continue-on-error: true 58 | 59 | - name: Send Scrutinizer data 60 | run: | 61 | wget https://scrutinizer-ci.com/ocular.phar 62 | php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml 63 | continue-on-error: true 64 | 65 | - name: Infection score report 66 | run: | 67 | vendor/bin/infection run -j 2 68 | continue-on-error: true 69 | 70 | - name: PHP Insights report 71 | run: | 72 | rm -rf composer.lock vendor 73 | composer require nunomaduro/phpinsights --dev 74 | vendor/bin/phpinsights analyse src/ -n 75 | continue-on-error: true 76 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at am@localheinz.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/AbstractBaseHtmlTagObject.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | interface AttributeInterface extends 19 | AlterableInterface, 20 | ArrayAccess, 21 | EscapableInterface, 22 | PreprocessableInterface, 23 | RenderableInterface, 24 | Serializable, 25 | StringableInterface 26 | { 27 | /** 28 | * {@inheritdoc} 29 | * 30 | * @return \drupol\htmltag\Attribute\AttributeInterface 31 | */ 32 | public function alter(callable ...$closures): AttributeInterface; 33 | 34 | /** 35 | * Append a value to the attribute. 36 | * 37 | * @param mixed[]|string|null ...$value 38 | * The value to append. 39 | * 40 | * @return \drupol\htmltag\Attribute\AttributeInterface 41 | * The attribute 42 | */ 43 | public function append(...$value): AttributeInterface; 44 | 45 | /** 46 | * Check if the attribute contains a string or a substring. 47 | * 48 | * @param mixed[]|string ...$substring 49 | * The string to check. 50 | * 51 | * @return bool 52 | * True or False 53 | */ 54 | public function contains(...$substring): bool; 55 | 56 | /** 57 | * Delete the current attribute. 58 | * 59 | * @return \drupol\htmltag\Attribute\AttributeInterface 60 | * The attribute 61 | */ 62 | public function delete(): AttributeInterface; 63 | 64 | /** 65 | * Get the attribute name. 66 | * 67 | * @return string 68 | * The attribute name 69 | */ 70 | public function getName(): string; 71 | 72 | /** 73 | * Get the attribute value as an array. 74 | * 75 | * @return array 76 | * The attribute value as an array 77 | */ 78 | public function getValuesAsArray(): array; 79 | 80 | /** 81 | * Get the attribute value as a string. 82 | * 83 | * @return string|null 84 | * The attribute value as a string 85 | */ 86 | public function getValuesAsString(): ?string; 87 | 88 | /** 89 | * Check if the attribute is a loner attribute. 90 | * 91 | * @return bool 92 | * True or False 93 | */ 94 | public function isBoolean(); 95 | 96 | /** 97 | * Remove a value from the attribute. 98 | * 99 | * @param array|string ...$value 100 | * The value to remove. 101 | * 102 | * @return \drupol\htmltag\Attribute\AttributeInterface 103 | * The attribute 104 | */ 105 | public function remove(...$value): AttributeInterface; 106 | 107 | /** 108 | * Replace a value of the attribute. 109 | * 110 | * @param mixed[]|string $original 111 | * The original value 112 | * @param mixed[]|string ...$replacement 113 | * The replacement value. 114 | * 115 | * @return \drupol\htmltag\Attribute\AttributeInterface 116 | * The attribute 117 | */ 118 | public function replace($original, ...$replacement): AttributeInterface; 119 | 120 | /** 121 | * Set the value. 122 | * 123 | * @param array|string|null ...$value 124 | * The value. 125 | * 126 | * @return \drupol\htmltag\Attribute\AttributeInterface 127 | * The attribute 128 | */ 129 | public function set(...$value): AttributeInterface; 130 | 131 | /** 132 | * Set the attribute as a loner attribute. 133 | * 134 | * @param bool $boolean 135 | * True or False 136 | * 137 | * @return \drupol\htmltag\Attribute\AttributeInterface 138 | * The attribute 139 | */ 140 | public function setBoolean($boolean = true): AttributeInterface; 141 | } 142 | -------------------------------------------------------------------------------- /src/Tag/AbstractTag.php: -------------------------------------------------------------------------------- 1 | tag = $name; 47 | $this->attributes = $attributes; 48 | $this->content($content); 49 | } 50 | 51 | /** 52 | * @param array $arguments 53 | * 54 | * @return \drupol\htmltag\Tag\TagInterface 55 | */ 56 | public static function __callStatic(string $name, array $arguments = []) 57 | { 58 | return new static($arguments[0], $name); 59 | } 60 | 61 | public function __toString(): string 62 | { 63 | return $this->render(); 64 | } 65 | 66 | public function alter(callable ...$closures): TagInterface 67 | { 68 | foreach ($closures as $closure) { 69 | $this->content = $closure( 70 | $this->ensureFlatArray((array) $this->content) 71 | ); 72 | } 73 | 74 | return $this; 75 | } 76 | 77 | public function attr(?string $name = null, ...$value) 78 | { 79 | if (null === $name) { 80 | return $this->attributes->render(); 81 | } 82 | 83 | if ([] === $value) { 84 | return $this->attributes[$name]; 85 | } 86 | 87 | return $this->attributes[$name]->set($value); 88 | } 89 | 90 | public function content(...$data): ?string 91 | { 92 | if ([] !== $data) { 93 | if (null === reset($data)) { 94 | $data = null; 95 | } 96 | 97 | $this->content = $data; 98 | } 99 | 100 | return $this->renderContent(); 101 | } 102 | 103 | public function escape($value): ?string 104 | { 105 | $return = $this->ensureString($value); 106 | 107 | if ($value instanceof StringableInterface) { 108 | return $return; 109 | } 110 | 111 | return null === $return ? 112 | $return : 113 | htmlentities($return); 114 | } 115 | 116 | /** 117 | * @return array 118 | */ 119 | public function getContentAsArray(): array 120 | { 121 | return $this->preprocess( 122 | $this->ensureFlatArray((array) $this->content) 123 | ); 124 | } 125 | 126 | public function preprocess(array $values, array $context = []): array 127 | { 128 | return $values; 129 | } 130 | 131 | public function render(): string 132 | { 133 | return null === ($content = $this->renderContent()) ? 134 | sprintf('<%s%s/>', $this->tag, $this->attributes->render()) : 135 | sprintf('<%s%s>%s', $this->tag, $this->attributes->render(), $content, $this->tag); 136 | } 137 | 138 | public function serialize() 139 | { 140 | return serialize([ 141 | 'tag' => $this->tag, 142 | 'attributes' => $this->attributes->getValuesAsArray(), 143 | 'content' => $this->renderContent(), 144 | ]); 145 | } 146 | 147 | public function unserialize($serialized) 148 | { 149 | $unserialize = unserialize($serialized); 150 | 151 | $this->tag = $unserialize['tag']; 152 | $this->attributes = $this->attributes->import($unserialize['attributes']); 153 | $this->content = $unserialize['content']; 154 | } 155 | 156 | /** 157 | * Render the tag content. 158 | */ 159 | protected function renderContent(): ?string 160 | { 161 | return [] === ($items = array_map([$this, 'escape'], $this->getContentAsArray())) ? 162 | null : 163 | implode('', $items); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Attributes/AttributesInterface.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | interface AttributesInterface extends 21 | ArrayAccess, 22 | Countable, 23 | IteratorAggregate, 24 | PreprocessableInterface, 25 | RenderableInterface, 26 | Serializable, 27 | StringableInterface 28 | { 29 | /** 30 | * Append a value into an attribute. 31 | * 32 | * @param string $key 33 | * The attribute's name 34 | * @param array|string ...$values 35 | * The attribute's values. 36 | * 37 | * @return \drupol\htmltag\Attributes\AttributesInterface 38 | * The attributes 39 | */ 40 | public function append($key, ...$values): AttributesInterface; 41 | 42 | /** 43 | * Check if attribute contains a value. 44 | * 45 | * @param string $key 46 | * Attribute name 47 | * @param mixed[]|string ...$values 48 | * Attribute values. 49 | * 50 | * @return bool 51 | * Whereas an attribute contains a value 52 | */ 53 | public function contains(string $key, ...$values): bool; 54 | 55 | /** 56 | * Delete an attribute. 57 | * 58 | * @param array|string ...$keys 59 | * The name(s) of the attribute to delete. 60 | * 61 | * @return \drupol\htmltag\Attributes\AttributesInterface 62 | * The attributes 63 | */ 64 | public function delete(string ...$keys): AttributesInterface; 65 | 66 | /** 67 | * Check if an attribute exists and if a value if provided check it as well. 68 | * 69 | * @param string $key 70 | * Attribute name 71 | * @param mixed|string ...$values 72 | * The value to check if the attribute name exists. 73 | * 74 | * @return bool 75 | * True if the attribute exists, false otherwise 76 | */ 77 | public function exists(string $key, ...$values): bool; 78 | 79 | /** 80 | * Get storage. 81 | * 82 | * @return ArrayIterator 83 | * The storage array 84 | */ 85 | public function getStorage(): ArrayIterator; 86 | 87 | /** 88 | * Get the values as an array. 89 | * 90 | * @return array 91 | * The attributes values keyed by the attribute name 92 | */ 93 | public function getValuesAsArray(): array; 94 | 95 | /** 96 | * Import attributes. 97 | * 98 | * @param array|Traversable $data 99 | * The data to import 100 | * 101 | * @return \drupol\htmltag\Attributes\AttributesInterface 102 | * The attributes 103 | */ 104 | public function import($data): AttributesInterface; 105 | 106 | /** 107 | * Merge attributes. 108 | * 109 | * @param array ...$dataset 110 | * The data to merge. 111 | * 112 | * @return \drupol\htmltag\Attributes\AttributesInterface 113 | * The attributes 114 | */ 115 | public function merge(array ...$dataset): AttributesInterface; 116 | 117 | /** 118 | * Remove a value from a specific attribute. 119 | * 120 | * @param string $key 121 | * The attribute's name 122 | * @param array|string ...$values 123 | * The attribute's values. 124 | * 125 | * @return \drupol\htmltag\Attributes\AttributesInterface 126 | * The attributes 127 | */ 128 | public function remove(string $key, ...$values): AttributesInterface; 129 | 130 | /** 131 | * Replace a value with another. 132 | * 133 | * @param string $key 134 | * The attributes's name 135 | * @param string $value 136 | * The attribute's value 137 | * @param string ...$replacements 138 | * The replacement values. 139 | * 140 | * @return \drupol\htmltag\Attributes\AttributesInterface 141 | * The attributes 142 | */ 143 | public function replace(string $key, string $value, string ...$replacements): AttributesInterface; 144 | 145 | /** 146 | * Set an attribute. 147 | * 148 | * @param string $key 149 | * The attribute name 150 | * @param string|null ...$values 151 | * The attribute values. 152 | * 153 | * @return \drupol\htmltag\Attributes\AttributesInterface 154 | * The attributes 155 | */ 156 | public function set(string $key, ...$values): AttributesInterface; 157 | 158 | /** 159 | * Return the attributes without a specific attribute. 160 | * 161 | * @param string ...$keys 162 | * The name(s) of the attribute to remove. 163 | * 164 | * @return \drupol\htmltag\Attributes\AttributesInterface 165 | * The attributes 166 | */ 167 | public function without(string ...$keys): AttributesInterface; 168 | } 169 | -------------------------------------------------------------------------------- /src/Attribute/AbstractAttribute.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | private $values; 35 | 36 | /** 37 | * Attribute constructor. 38 | * 39 | * @param string $name 40 | * The attribute name 41 | * @param mixed[]|string|string[] ...$values 42 | * The attribute values. 43 | */ 44 | public function __construct(string $name, ...$values) 45 | { 46 | if (1 === preg_match('/[\t\n\f \/>"\'=]+/', $name)) { 47 | // @todo: create exception class for this. 48 | throw new InvalidArgumentException('Attribute name is not valid.'); 49 | } 50 | 51 | $this->name = $name; 52 | $this->values = $values; 53 | } 54 | 55 | public function __toString(): string 56 | { 57 | return $this->render(); 58 | } 59 | 60 | public function alter(callable ...$closures): AttributeInterface 61 | { 62 | foreach ($closures as $closure) { 63 | $this->values = $closure( 64 | $this->ensureFlatArray($this->values), 65 | $this->name 66 | ); 67 | } 68 | 69 | return $this; 70 | } 71 | 72 | public function append(...$value): AttributeInterface 73 | { 74 | $this->values[] = $value; 75 | 76 | return $this; 77 | } 78 | 79 | public function contains(...$substring): bool 80 | { 81 | $values = $this->ensureFlatArray($this->values); 82 | 83 | return !in_array( 84 | false, 85 | array_map( 86 | static function ($substring_item) use ($values) { 87 | return in_array($substring_item, $values, true); 88 | }, 89 | $this->ensureFlatArray($substring) 90 | ), 91 | true 92 | ); 93 | } 94 | 95 | public function delete(): AttributeInterface 96 | { 97 | $this->name = ''; 98 | $this->values = []; 99 | 100 | return $this; 101 | } 102 | 103 | public function escape($value): string 104 | { 105 | return null === $value ? 106 | $value : 107 | htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE); 108 | } 109 | 110 | public function getName(): string 111 | { 112 | return $this->name; 113 | } 114 | 115 | public function getValuesAsArray(): array 116 | { 117 | return $this->ensureStrings( 118 | $this->preprocess( 119 | $this->ensureFlatArray($this->values), 120 | ['name' => $this->name] 121 | ) 122 | ); 123 | } 124 | 125 | public function getValuesAsString(): ?string 126 | { 127 | return [] === ($values = $this->getValuesAsArray()) ? 128 | null : 129 | (string) $this->escape(implode(' ', array_filter($values, '\strlen'))); 130 | } 131 | 132 | public function isBoolean(): bool 133 | { 134 | return [] === $this->getValuesAsArray(); 135 | } 136 | 137 | /** 138 | * @param int $offset 139 | * 140 | * @return bool 141 | */ 142 | public function offsetExists($offset) 143 | { 144 | return $this->contains((string) $offset); 145 | } 146 | 147 | /** 148 | * @param int $offset 149 | * 150 | * @return void 151 | */ 152 | public function offsetGet($offset) 153 | { 154 | throw new BadMethodCallException('Unsupported method.'); 155 | } 156 | 157 | /** 158 | * @param int $offset 159 | * @param mixed $value 160 | * 161 | * @return void 162 | */ 163 | public function offsetSet($offset, $value) 164 | { 165 | $this->append($value); 166 | } 167 | 168 | /** 169 | * @param int $offset 170 | * 171 | * @return void 172 | */ 173 | public function offsetUnset($offset) 174 | { 175 | $this->remove((string) $offset); 176 | } 177 | 178 | public function preprocess(array $values, array $context = []): array 179 | { 180 | return $values; 181 | } 182 | 183 | public function remove(...$value): AttributeInterface 184 | { 185 | return $this->set( 186 | array_diff( 187 | $this->ensureFlatArray($this->values), 188 | $this->ensureFlatArray($value) 189 | ) 190 | ); 191 | } 192 | 193 | public function render(): string 194 | { 195 | return null === ($values = $this->getValuesAsString()) ? 196 | $this->name : 197 | $this->name . '="' . $values . '"'; 198 | } 199 | 200 | public function replace($original, ...$replacement): AttributeInterface 201 | { 202 | $count_start = count($this->ensureFlatArray($this->values)); 203 | $this->remove($original); 204 | 205 | if (count($this->ensureFlatArray($this->values)) !== $count_start) { 206 | $this->append($replacement); 207 | } 208 | 209 | return $this; 210 | } 211 | 212 | public function serialize() 213 | { 214 | return serialize([ 215 | 'name' => $this->name, 216 | 'values' => $this->getValuesAsArray(), 217 | ]); 218 | } 219 | 220 | public function set(...$value): AttributeInterface 221 | { 222 | $this->values = $value; 223 | 224 | return $this; 225 | } 226 | 227 | public function setBoolean($boolean = true): AttributeInterface 228 | { 229 | return true === $boolean ? 230 | $this->set() : 231 | $this->append(''); 232 | } 233 | 234 | public function unserialize($serialized) 235 | { 236 | $unserialized = unserialize($serialized); 237 | 238 | $this->name = $unserialized['name']; 239 | $this->values = $unserialized['values']; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/Attributes/AbstractAttributes.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | private $storage = []; 28 | 29 | /** 30 | * Attributes constructor. 31 | * 32 | * @param \drupol\htmltag\Attribute\AttributeFactoryInterface $attributeFactory 33 | * The attribute factory 34 | * @param mixed[] $data 35 | * The input attributes 36 | */ 37 | public function __construct(AttributeFactoryInterface $attributeFactory, array $data = []) 38 | { 39 | $this->attributeFactory = $attributeFactory; 40 | $this->import($data); 41 | } 42 | 43 | public function __toString(): string 44 | { 45 | return $this->render(); 46 | } 47 | 48 | public function append($key, ...$values): AttributesInterface 49 | { 50 | $this->storage += [ 51 | $key => $this->attributeFactory->getInstance($key), 52 | ]; 53 | 54 | $this->storage[$key]->append($values); 55 | 56 | return $this; 57 | } 58 | 59 | public function contains(string $key, ...$values): bool 60 | { 61 | return $this->exists($key) && $this->storage[$key]->contains(...$values); 62 | } 63 | 64 | public function count() 65 | { 66 | return $this->getStorage()->count(); 67 | } 68 | 69 | public function delete(string ...$keys): AttributesInterface 70 | { 71 | foreach ($this->ensureStrings($this->ensureFlatArray($keys)) as $key) { 72 | unset($this->storage[$key]); 73 | } 74 | 75 | return $this; 76 | } 77 | 78 | public function exists(string $key, ...$values): bool 79 | { 80 | if (!isset($this->storage[$key])) { 81 | return false; 82 | } 83 | 84 | return [] === $values ? 85 | true : 86 | $this->contains($key, $values); 87 | } 88 | 89 | public function getIterator() 90 | { 91 | return $this->getStorage(); 92 | } 93 | 94 | public function getStorage(): ArrayIterator 95 | { 96 | return new ArrayIterator(array_values($this->preprocess($this->storage))); 97 | } 98 | 99 | public function getValuesAsArray(): array 100 | { 101 | $values = []; 102 | 103 | foreach ($this->getStorage() as $attribute) { 104 | $values[$attribute->getName()] = $attribute->getValuesAsArray(); 105 | } 106 | 107 | return $values; 108 | } 109 | 110 | public function import($data): AttributesInterface 111 | { 112 | foreach ($data as $key => $value) { 113 | $this->storage[$key] = $this->attributeFactory->getInstance($key, $value); 114 | } 115 | 116 | return $this; 117 | } 118 | 119 | public function merge(array ...$dataset): AttributesInterface 120 | { 121 | foreach ($dataset as $data) { 122 | foreach ($data as $key => $value) { 123 | $this->append($key, $value); 124 | } 125 | } 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * @param int $key 132 | * 133 | * @return bool 134 | */ 135 | public function offsetExists($key) 136 | { 137 | return isset($this->storage[$key]); 138 | } 139 | 140 | /** 141 | * @param int $key 142 | * 143 | * @return mixed 144 | */ 145 | public function offsetGet($key) 146 | { 147 | $this->storage += [ 148 | $key => $this->attributeFactory->getInstance((string) $key), 149 | ]; 150 | 151 | return $this->storage[$key]; 152 | } 153 | 154 | /** 155 | * @param int $key 156 | * @param mixed $value 157 | * 158 | * @return void 159 | */ 160 | public function offsetSet($key, $value) 161 | { 162 | $this->storage[$key] = $this->attributeFactory->getInstance((string) $key, $value); 163 | } 164 | 165 | /** 166 | * @param int $key 167 | * 168 | * @return void 169 | */ 170 | public function offsetUnset($key) 171 | { 172 | unset($this->storage[$key]); 173 | } 174 | 175 | public function preprocess(array $values, array $context = []): array 176 | { 177 | return $values; 178 | } 179 | 180 | public function remove(string $key, ...$values): AttributesInterface 181 | { 182 | if (array_key_exists($key, $this->storage)) { 183 | $this->storage[$key]->remove(...$values); 184 | } 185 | 186 | return $this; 187 | } 188 | 189 | public function render(): string 190 | { 191 | $output = ''; 192 | 193 | foreach ($this->getStorage() as $attribute) { 194 | $output .= ' ' . $attribute; 195 | } 196 | 197 | return $output; 198 | } 199 | 200 | public function replace(string $key, string $value, string ...$replacements): AttributesInterface 201 | { 202 | if (!$this->contains($key, $value)) { 203 | return $this; 204 | } 205 | 206 | $this->storage[$key]->replace($value, ...$replacements); 207 | 208 | return $this; 209 | } 210 | 211 | public function serialize() 212 | { 213 | return serialize([ 214 | 'storage' => $this->getValuesAsArray(), 215 | ]); 216 | } 217 | 218 | public function set(string $key, ...$value): AttributesInterface 219 | { 220 | $this->storage[$key] = $this->attributeFactory->getInstance($key, $value); 221 | 222 | return $this; 223 | } 224 | 225 | public function unserialize($serialized) 226 | { 227 | $unserialize = unserialize($serialized); 228 | $attributeFactory = $this->attributeFactory; 229 | 230 | $this->storage = array_map( 231 | static function ($key, $values) use ($attributeFactory) { 232 | return $attributeFactory::build((string) $key, $values); 233 | }, 234 | array_keys($unserialize['storage']), 235 | array_values($unserialize['storage']) 236 | ); 237 | } 238 | 239 | public function without(string ...$keys): AttributesInterface 240 | { 241 | $attributes = clone $this; 242 | 243 | return $attributes->delete(...$keys); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version][latest stable version]][packagist collection] 2 | [![GitHub stars][github stars]][packagist collection] 3 | [![Total Downloads][total downloads]][packagist collection] 4 | [![GitHub Workflow Status][github workflow status]][collection actions] 5 | [![Scrutinizer code quality][code quality]][scrutinizer code quality] 6 | [![Type Coverage][type coverage]][sheperd type coverage] 7 | [![Code Coverage][code coverage]][scrutinizer code quality] 8 | [![License][license]][packagist collection] 9 | [![Donate!][donate github]][github sponsor] 10 | [![Donate!][donate paypal]][paypal sponsor] 11 | 12 | # HTMLTag 13 | 14 | ## Description 15 | 16 | This is a PHP library that handles the generation of HTML tags, their attributes and content. 17 | 18 | The focus is on security, speed and usability. 19 | 20 | ## Requirements 21 | 22 | * PHP >= 7.1.3 23 | 24 | ## Installation 25 | 26 | ```composer require drupol/htmltag``` 27 | 28 | ## Usage 29 | 30 | ```php 31 | 'author']); 37 | $meta->attr('content', 'pol dellaiera'); 38 | 39 | // Title object. 40 | $title = \drupol\htmltag\HtmlTag::tag('h1', ['class' => 'title'], 'Welcome to HTMLTag'); 41 | 42 | // Paragraph object. 43 | $paragraph = \drupol\htmltag\HtmlTag::tag('p', ['class' => 'section']); 44 | $paragraph->attr('class')->append('paragraph'); 45 | $paragraph->content('This library helps you create HTML.'); 46 | 47 | // Simple footer 48 | $footer = \drupol\htmltag\HtmlTag::tag('footer', [], 'Thanks for using it!'); 49 | 50 | // Body tag. 51 | // Add content that can be transformed into strings. 52 | $body = \drupol\htmltag\HtmlTag::tag('body', [], [$title, $paragraph, $footer]); 53 | 54 | // Fix something that was already added. 55 | $paragraph->attr('class')->remove('section')->replace('paragraph', 'description'); 56 | 57 | // Alter the values of a specific attributes. 58 | $meta->attr('content')->alter( 59 | function ($values) { 60 | return array_map('strtoupper', $values); 61 | } 62 | ); 63 | 64 | echo $meta . $body; 65 | ``` 66 | 67 | Will print: 68 | 69 | ```html 70 | 71 | 72 |

Welcome to HTMLTag

73 |

This library helps you create HTML.

74 |
Thanks for using it!
75 | 76 | ``` 77 | 78 | # HTML Builder 79 | 80 | The library comes with an HTML Builder class that allows you to quickly create HTML content. 81 | 82 | ```php 83 | c(' Comment 1 ') // Add a comment 91 | ->p(['class' => ['paragraph']], 'some content') 92 | ->div(['class' => 'container'], 'this is a simple div') 93 | ->_() // End tag
94 | ->c(' Comment 2 ') 95 | ->region([], 'region with tags') 96 | ->_() 97 | ->c(' Comment 3 ') 98 | ->a() 99 | ->c(' Comment 4 ') 100 | ->span(['class' => 'link'], 'Link content') 101 | ->_() 102 | ->div(['class' => 'Unsecure "classes"'], 'Unsecure content') 103 | ->_() 104 | ->c(' Comment 5 '); 105 | 106 | echo $html; 107 | ``` 108 | 109 | This will produce: 110 | 111 | ```html 112 | 113 |

114 | some content 115 |

116 | this is a simple div 117 |
118 |

119 | 120 | 121 | region with <unsafe> tags 122 | 123 | 124 | 125 | 126 | 127 | Link content 128 | 129 | 130 |
131 | Unsecure <a href="#">content</a> 132 |
133 | 134 | ``` 135 | 136 | ## Technical notes 137 | 138 | ### Tag analysis 139 | 140 | ``` 141 | The tag name An attribute The content 142 | | | | 143 | ++-+ +-----+-----+ +----+-----+ 144 | | | | | | | 145 | 146 | Hello world! 147 | 148 | | | 149 | +-----------------------+-----------------------+ 150 | | 151 | The attributes 152 | ``` 153 | 154 | The library is built around 3 objects. 155 | 156 | * The Tag object that handles the attributes, the tag name and the content, 157 | * The Attributes object that handles the attributes, 158 | * The Attribute object that handles an attribute which is composed of name and its value(s). 159 | 160 | The Tag object uses the Attributes object which is, basically, the storage of Attribute objects. 161 | You may use each of these objects individually. 162 | 163 | All methods are documented through interfaces and your IDE should be able to autocomplete when needed. 164 | 165 | Most methods parameters are [variadics](http://php.net/manual/en/functions.arguments.php#functions.variable-arg-list) and 166 | accept unlimited nested values or array of values. 167 | You can also chain most of the methods. 168 | 169 | The allowed type of values can be almost anything. If it's an object, it must implements the `__toString()` method. 170 | 171 | #### Examples 172 | 173 | Method chaining: 174 | 175 | ```php 176 | attr('class', ['FRONT', ['NODE', ['sidebra']], 'node', ' a', ' b ', [' c']]) 183 | ->replace('sidebra', 'sidebar') 184 | ->alter( 185 | function ($values) { 186 | $values = array_map('strtolower', $values); 187 | $values = array_unique($values); 188 | $values = array_map('trim', $values); 189 | natcasesort($values); 190 | 191 | return $values; 192 | } 193 | ); 194 | $tag->content('Hello world'); 195 | 196 | echo $tag; // Hello world 197 | ``` 198 | 199 | The following examples will all produce the same HTML. 200 | 201 | ```php 202 | attr('class', ['front', ['node', ['sidebar']]]); 208 | $tag->content('Hello world'); 209 | 210 | echo $tag; // Hello world 211 | ``` 212 | 213 | ```php 214 | attr('class', 'front', 'node', 'sidebar'); 220 | $tag->content('Hello world'); 221 | 222 | echo $tag; // Hello world 223 | ``` 224 | 225 | ```php 226 | attr('class', ['front', 'node', 'sidebar']); 232 | $tag->content('Hello world'); 233 | 234 | echo $tag; // Hello world 235 | ``` 236 | 237 | ```php 238 | attr('class', 'front node sidebar'); 244 | $tag->content('Hello world'); 245 | 246 | echo $tag; // Hello world 247 | ``` 248 | 249 | ### Tag object 250 | 251 | ```php 252 | attr('class', 'front'); 258 | $tag->content('Hello world'); 259 | 260 | echo $tag; // Hello world 261 | ``` 262 | 263 | ### Attributes object 264 | 265 | ```php 266 | append('class', 'a', 'b', 'c'); 272 | $attributes->append('id', 'htmltag'); 273 | 274 | // Hence the trailing space before the class attribute. 275 | echo $attributes; // class="a b c" id="htmltag" 276 | ``` 277 | 278 | ### Attribute object 279 | 280 | ```php 281 | attr('class', 'E', 'C', ['A', 'B'], 'D', 'A', ' F '); 326 | // Add a random attribute and the same values. 327 | $tag->attr('data-original', 'e', 'c', ['a', 'b'], 'd', 'a', ' f '); 328 | 329 | echo $tag; //

330 | ``` 331 | 332 | The same mechanism goes for the `Tag` class. 333 | 334 | ## Security 335 | 336 | To avoid security issues, every printed objects are escaped. 337 | 338 | If objects are used as input and if they implement the `__toString()` method, they will be converted to string. 339 | It's up to the user to make sure that they print **unsafe** output so they are not escaped twice. 340 | 341 | ## Code quality, tests and benchmarks 342 | 343 | Every time changes are introduced into the library, [Travis CI](https://travis-ci.org/drupol/htmltag/builds) run the tests and the benchmarks. 344 | 345 | The library has tests written with [PHPSpec](http://www.phpspec.net/). 346 | Feel free to check them out in the `spec` directory. Run `composer phpspec` to trigger the tests. 347 | 348 | [PHPBench](https://github.com/phpbench/phpbench) is used to benchmark the library, to run the benchmarks: `composer bench` 349 | 350 | [PHPInfection](https://github.com/infection/infection) is used to ensure that your code is properly tested, run `composer infection` to test your code. 351 | 352 | ## Contributing 353 | 354 | Feel free to contribute to this library by sending Github pull requests. I'm quite reactive :-) 355 | 356 | [packagist collection]: https://packagist.org/packages/drupol/htmltag 357 | [latest stable version]: https://img.shields.io/packagist/v/drupol/htmltag.svg?style=flat-square 358 | [github stars]: https://img.shields.io/github/stars/drupol/htmltag.svg?style=flat-square 359 | [total downloads]: https://img.shields.io/packagist/dt/drupol/htmltag.svg?style=flat-square 360 | [github workflow status]: https://img.shields.io/github/workflow/status/drupol/htmltag/Continuous%20Integration?style=flat-square 361 | [code quality]: https://img.shields.io/scrutinizer/quality/g/drupol/htmltag/master.svg?style=flat-square 362 | [scrutinizer code quality]: https://scrutinizer-ci.com/g/drupol/htmltag/?branch=master 363 | [type coverage]: https://shepherd.dev/github/drupol/htmltag/coverage.svg 364 | [sheperd type coverage]: https://shepherd.dev/github/drupol/htmltag 365 | [code coverage]: https://img.shields.io/scrutinizer/coverage/g/drupol/htmltag/master.svg?style=flat-square 366 | [license]: https://img.shields.io/packagist/l/drupol/htmltag.svg?style=flat-square 367 | [donate github]: https://img.shields.io/badge/Sponsor-Github-brightgreen.svg?style=flat-square 368 | [donate paypal]: https://img.shields.io/badge/Sponsor-Paypal-brightgreen.svg?style=flat-square 369 | [github sponsor]: https://github.com/sponsors/drupol 370 | [paypal sponsor]: https://www.paypal.me/drupol 371 | [collection actions]: https://github.com/drupol/htmltag/actions --------------------------------------------------------------------------------