From 074b41167e4c519d61d860bbeb585c71a3a5592e Mon Sep 17 00:00:00 2001 From: Joby Elliott Date: Fri, 8 Sep 2023 01:19:39 +0000 Subject: [PATCH] lots of new tags and features in progress --- .devcontainer/Dockerfile | 16 + .devcontainer/devcontainer.json | 34 ++ .github/workflows/dependency-review.yml | 20 - .github/workflows/tests.yaml | 4 +- LICENSE | 2 +- composer.json | 7 +- phpcs.xml | 19 - src/AbstractParser.php | 38 +- src/Helpers/Attributes.php | 148 ++++++- src/Helpers/StringableEnumArray.php | 49 +++ .../ContentSectioningTags/AddressTag.php | 14 + .../ContentSectioningTags/ArticleTag.php | 11 + src/Html5/ContentSectioningTags/AsideTag.php | 8 + src/Html5/ContentSectioningTags/FooterTag.php | 9 + src/Html5/ContentSectioningTags/H1Tag.php | 9 + src/Html5/ContentSectioningTags/H2Tag.php | 9 + src/Html5/ContentSectioningTags/H3Tag.php | 9 + src/Html5/ContentSectioningTags/H4Tag.php | 9 + src/Html5/ContentSectioningTags/H5Tag.php | 9 + src/Html5/ContentSectioningTags/H6Tag.php | 9 + src/Html5/ContentSectioningTags/HeaderTag.php | 8 + src/Html5/ContentSectioningTags/MainTag.php | 9 + src/Html5/ContentSectioningTags/NavTag.php | 9 + src/Html5/ContentSectioningTags/SearchTag.php | 24 ++ .../ContentSectioningTags/SectionTag.php | 8 + src/Html5/DocumentTags/BodyTag.php | 7 + src/Html5/DocumentTags/Doctype.php | 11 + src/Html5/DocumentTags/HeadTag.php | 7 + src/Html5/DocumentTags/HtmlTag.php | 8 + src/Html5/DocumentTags/TitleTag.php | 12 +- src/Html5/Enums/As_link.php | 63 +++ src/Html5/Enums/Autocomplete.php | 313 ++++++++++++++ src/Html5/Enums/BrowsingContext.php | 34 ++ src/Html5/Enums/Capture.php | 29 ++ src/Html5/Enums/CrossOrigin.php | 30 ++ src/Html5/Enums/Draggable.php | 9 + src/Html5/Enums/HttpEquiv_meta.php | 48 +++ src/Html5/Enums/InputMode.php | 61 +++ src/Html5/Enums/Name_meta.php | 85 ++++ src/Html5/Enums/ReferrerPolicy_link.php | 41 ++ src/Html5/Enums/Rel_a.php | 87 ++++ src/Html5/Enums/Rel_form.php | 65 +++ src/Html5/Enums/Rel_link.php | 106 +++++ src/Html5/Enums/Robots_meta.php | 50 +++ src/Html5/Enums/Spellcheck.php | 9 + src/Html5/Enums/Translate.php | 18 + src/Html5/Enums/Type_list.php | 11 + .../Exceptions/InvalidArgumentsException.php | 9 + .../Exceptions/InvalidStateException.php | 9 + src/Html5/Html5Parser.php | 3 + src/Html5/Tags/BaseTag.php | 69 ++- src/Html5/Tags/HgroupTag.php | 10 +- src/Html5/Tags/LinkTag.php | 395 ++++++++++++++++-- src/Html5/Tags/MetaTag.php | 203 +++++---- src/Html5/Tags/NoscriptTag.php | 6 + src/Html5/Tags/ScriptTag.php | 18 +- src/Html5/Tags/StyleTag.php | 10 +- src/Html5/TextContentTags/BlockquoteTag.php | 8 +- src/Html5/TextContentTags/DdTag.php | 6 + src/Html5/TextContentTags/DivTag.php | 6 + src/Html5/TextContentTags/DlTag.php | 6 + src/Html5/TextContentTags/DtTag.php | 6 + src/Html5/TextContentTags/FigcaptionTag.php | 6 + src/Html5/TextContentTags/FigureTag.php | 6 + src/Html5/TextContentTags/HrTag.php | 6 + src/Html5/TextContentTags/LiTag.php | 46 +- src/Html5/TextContentTags/MenuTag.php | 6 + src/Html5/TextContentTags/OlTag.php | 91 +++- src/Html5/TextContentTags/PTag.php | 9 + src/Html5/TextContentTags/PreTag.php | 9 + src/Html5/TextContentTags/SpanTag.php | 25 ++ src/Html5/TextContentTags/UlTag.php | 7 + src/Tags/AbstractContainerTag.php | 11 +- src/Tags/AbstractContentTag.php | 13 +- src/Tags/AbstractGroupedTag.php | 17 +- tests/Helpers/AttributesTest.php | 6 + tests/Html5/DocumentTags/TitleTagTest.php | 2 +- tests/Html5/GenericHtmlDocumentTest.php | 2 +- tests/Html5/ParserTest.php | 23 +- tests/Html5/Tags/LinkTagTest.php | 15 +- tests/Html5/Tags/MetaTagTest.php | 9 +- tests/Html5/Tags/TagTestCase.php | 76 ++-- tests/Html5/TextContentTags/LiTagTest.php | 25 ++ tests/Html5/TextContentTags/OlTagTest.php | 16 +- tests/Html5/parser_documents/.gitignore | 1 + .../Html5/parser_documents/full-document.html | 13 + .../Html5/parser_documents/full-document.yaml | 15 + tests/Html5/parser_documents/normal-html.html | 10 + tests/Html5/parser_documents/normal-html.yaml | 12 + .../parser_documents/re-rendered/README.md | 3 + tests/Tags/AbstractContainerTagTest.php | 28 +- 91 files changed, 2584 insertions(+), 283 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json delete mode 100644 .github/workflows/dependency-review.yml delete mode 100644 phpcs.xml create mode 100644 src/Helpers/StringableEnumArray.php create mode 100644 src/Html5/ContentSectioningTags/SearchTag.php create mode 100644 src/Html5/Enums/As_link.php create mode 100644 src/Html5/Enums/Autocomplete.php create mode 100644 src/Html5/Enums/BrowsingContext.php create mode 100644 src/Html5/Enums/Capture.php create mode 100644 src/Html5/Enums/CrossOrigin.php create mode 100644 src/Html5/Enums/Draggable.php create mode 100644 src/Html5/Enums/HttpEquiv_meta.php create mode 100644 src/Html5/Enums/InputMode.php create mode 100644 src/Html5/Enums/Name_meta.php create mode 100644 src/Html5/Enums/ReferrerPolicy_link.php create mode 100644 src/Html5/Enums/Rel_a.php create mode 100644 src/Html5/Enums/Rel_form.php create mode 100644 src/Html5/Enums/Rel_link.php create mode 100644 src/Html5/Enums/Robots_meta.php create mode 100644 src/Html5/Enums/Spellcheck.php create mode 100644 src/Html5/Enums/Translate.php create mode 100644 src/Html5/Enums/Type_list.php create mode 100644 src/Html5/Exceptions/InvalidArgumentsException.php create mode 100644 src/Html5/Exceptions/InvalidStateException.php create mode 100644 src/Html5/TextContentTags/SpanTag.php create mode 100644 tests/Html5/TextContentTags/LiTagTest.php create mode 100644 tests/Html5/parser_documents/.gitignore create mode 100644 tests/Html5/parser_documents/full-document.html create mode 100644 tests/Html5/parser_documents/full-document.yaml create mode 100644 tests/Html5/parser_documents/normal-html.html create mode 100644 tests/Html5/parser_documents/normal-html.yaml create mode 100644 tests/Html5/parser_documents/re-rendered/README.md diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..036f3ee --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:22.04 + +# prepare to install php 8.2 +RUN apt update && apt install -y software-properties-common +RUN add-apt-repository ppa:ondrej/php +RUN apt update + +# install php 8.2 and other fundamental packages +RUN export DEBIAN_FRONTEND=noninteractive; apt install -y --no-install-recommends php8.2 php-curl git openssl unzip + +# install composer and its CA certificates +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer +COPY --from=composer:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +# install the PHP extensions that basically all PHP projects should need +RUN export DEBIAN_FRONTEND=noninteractive; apt install -y php8.2-opcache php-xdebug php-mbstring php-zip php-gd php-xml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9a605e2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +{ + // build from dockerfile + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + // specify run arguments + "runArgs": [ + "--dns=8.8.8.8" // for some reason DNS doesn't work right unless we explicitly name a DNS server + ], + // mount entire sites_v2 directory, so we can access global config and shared DB + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace/${localWorkspaceFolderBasename},type=bind,consistency=cached", + "workspaceFolder": "/workspace/${localWorkspaceFolderBasename}", + // specify extensions that we want + "customizations": { + "vscode": { + "extensions": [ + "DEVSENSE.intelli-php-vscode", + "DEVSENSE.phptools-vscode", + "DEVSENSE.profiler-php-vscode", + "DEVSENSE.composer-php-vscode", + "SanderRonde.phpstan-vscode", + "mrmlnc.vscode-scss", + "Gruntfuggly.todo-tree", + "ecmel.vscode-html-css", + "yzhang.markdown-all-in-one", + "DavidAnson.vscode-markdownlint", + "helixquar.randomeverything", + "neilbrayfield.php-docblocker", + "ms-vscode.test-adapter-converter" + ] + } + } +} \ No newline at end of file diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index fe461b4..0000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Dependency Review Action -# -# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. -# -# Source repository: https://github.com/actions/dependency-review-action -# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement -name: 'Dependency Review' -on: [pull_request] - -permissions: - contents: read - -jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: 'Checkout Repository' - uses: actions/checkout@v3 - - name: 'Dependency Review' - uses: actions/dependency-review-action@v2 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d97c609..d79847e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -10,6 +10,8 @@ jobs: dev: yes - uses: php-actions/phpstan@v3 with: + path: src + level: max memory_limit: 1G args: --memory-limit 1G - - uses: php-actions/phpunit@v3 \ No newline at end of file + - uses: php-actions/phpunit@v3 diff --git a/LICENSE b/LICENSE index 6d682eb..0bbff52 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License +# MIT License Copyright (c) 2019 Joby Elliott diff --git a/composer.json b/composer.json index f5a1fbb..39b485b 100644 --- a/composer.json +++ b/composer.json @@ -27,12 +27,11 @@ }, "scripts": { "test": "phpunit", - "stan": "phpstan", - "sniff": "phpcs" + "stan": "phpstan" }, "require-dev": { "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.7" + "mustangostang/spyc": "^0.6.3" } -} \ No newline at end of file +} diff --git a/phpcs.xml b/phpcs.xml deleted file mode 100644 index 5064e75..0000000 --- a/phpcs.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - Coding Standard - - src - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/AbstractParser.php b/src/AbstractParser.php index 73eef23..28cea59 100644 --- a/src/AbstractParser.php +++ b/src/AbstractParser.php @@ -19,12 +19,34 @@ use DOMElement; use DOMNode; use DOMText; +/** + * Extensions of this class to create new parsers are primarily meant to be + * controlled by adjusting the default properties below + */ abstract class AbstractParser { - /** @var array */ + /** + * A list of namespaces in which to match tags. To match in this way the + * tag classes must implement TagInterface and be named following the + * convention of the tag name in CamelCase followed by "Tag" i.e. a class + * implementing a tag would need to be named MyTagTag. + * + * They are searched in order and the first match is used. + * + * @var array + */ protected $tag_namespaces = []; - /** @var array> */ + /** + * A list of defined tag classes, matching a tag string (in lower case) to + * a fully-qualified TagInterface-implementing class. This array is also + * used at runtime to cache the pairings found and verified from + * $tag_namespaces. + * + * They are searched in order and the first match is used. + * + * @var array> + */ protected $tag_classes = []; /** @var class-string */ @@ -97,11 +119,13 @@ abstract class AbstractParser } elseif ($node instanceof DOMComment) { return new ($this->comment_class)($node->textContent); } elseif ($node instanceof DOMText) { - return new ($this->text_class)($node->textContent); + $content = trim($node->textContent); + if ($content) { + return new ($this->text_class)($content); + } } - // This line shouldn't be reached, but if it is it's philosophically - // consistent to simply ignore unknown node types - return null; // @codeCoverageIgnore + // It's philosophically consistent to simply ignore unknown node types + return null; } protected function convertNodeToTag(DOMElement $node): null|NodeInterface @@ -112,7 +136,7 @@ abstract class AbstractParser return null; } $tag = new $class(); - // tool for settin gup content tags + // tool for setting up content tags if ($tag instanceof ContentTagInterface) { $tag->setContent($node->textContent); } diff --git a/src/Helpers/Attributes.php b/src/Helpers/Attributes.php index 405fbcd..17ce3d5 100644 --- a/src/Helpers/Attributes.php +++ b/src/Helpers/Attributes.php @@ -4,6 +4,7 @@ namespace ByJoby\HTML\Helpers; use ArrayAccess; use ArrayIterator; +use BackedEnum; use Exception; use IteratorAggregate; use Stringable; @@ -12,12 +13,12 @@ use Traversable; /** * Holds and validates a set of HTML attribute name/value pairs for use in tags. * - * @implements ArrayAccess - * @implements IteratorAggregate + * @implements ArrayAccess + * @implements IteratorAggregate */ class Attributes implements IteratorAggregate, ArrayAccess { - /** @var array */ + /** @var array */ protected $array = []; /** @var bool */ protected $sorted = true; @@ -25,7 +26,7 @@ class Attributes implements IteratorAggregate, ArrayAccess protected $disallowed = []; /** - * @param null|array $array + * @param null|array $array * @param array $disallowed * @return void */ @@ -67,16 +68,147 @@ class Attributes implements IteratorAggregate, ArrayAccess $this->array[$offset] = $value; } - public function string(string $offset): null|string + /** + * Set a value as a stringable enum array, automatically converting from a single enum or normal array of enums. + * + * @template T of BackedEnum + * @param string $offset + * @param null|BackedEnum|StringableEnumArray|array $value + * @param class-string $enum_class + * @param string $separator + * @return static + */ + public function setEnumArray(string $offset, null|BackedEnum|StringableEnumArray|array $value, string $enum_class, string $separator): static + { + if (is_null($value)) { + $value = []; + } + if ($value instanceof BackedEnum) { + $value = [$value]; + } + if (is_array($value)) { + $value = new StringableEnumArray($value, ' '); + } + $this->offsetSet($offset, $value); + return $this; + } + + /** + * Returns a given offset's value as an array of enums. + * + * @template T of BackedEnum + * @param string $offset + * @param class-string $enum_class + * @param non-empty-string $separator + * @return array + */ + public function asEnumArray(string $offset, string $enum_class, string $separator): array + { + $value = strval($this->offsetGet($offset)); + $value = explode($separator, $value); + $value = array_map( + $enum_class::tryFrom(...), + $value + ); + $value = array_filter( + $value, + fn($e) => !empty($e) + ); + return $value; + } + + /** + * Returns a given offset's value as a string, if possible. + * + * @param string $offset + * @return null|string|Stringable + */ + public function asString(string $offset): null|string|Stringable { $value = $this->offsetGet($offset); - if (is_string($value)) { + if ($value instanceof Stringable || is_string($value)) { return $value; } else { return null; } } + /** + * Returns a given offset's value as an integer, if possible. + * + * @param string $offset + * @return null|int + */ + public function asInt(string $offset): null|int + { + $value = $this->asNumber($offset); + if (is_int($value)) { + return $value; + } else { + return null; + } + } + + /** + * Returns a given offset's value as a float, if possible. + * + * @param string $offset + * @return null|float + */ + public function asFloat(string $offset): null|float + { + $value = $this->asNumber($offset); + if (!is_null($value)) { + return floatval($value); + } else { + return null; + } + } + + /** + * Returns a given offset's value as a numeric type, if possible. + * + * @param string $offset + * @return null|number + */ + public function asNumber(string $offset): null|int|float + { + $value = $this->offsetGet($offset); + if (is_numeric($value)) { + if (is_string($value)) { + if ($value == intval($value)) { + $value = intval($value); + } else { + $value = floatval($value); + } + } + return $value; + } else { + return null; + } + } + + /** + * Return a given offset's value as an enum of the given class, if possible. + * + * @template T of BackedEnum + * @param string $offset + * @param class-string $enum_class + * @return null|T + */ + public function asEnum(string $offset, string $enum_class): null|BackedEnum + { + $value = $this->offsetGet($offset); + if ($value instanceof Stringable) { + $value = $value->__toString(); + } + if (is_string($value) || is_int($value)) { + return $enum_class::tryFrom($value); + } else { + return null; + } + } + public function offsetUnset(mixed $offset): void { $offset = static::sanitizeOffset($offset); @@ -84,7 +216,7 @@ class Attributes implements IteratorAggregate, ArrayAccess } /** - * @return array + * @return array */ public function getArray(): array { @@ -109,4 +241,4 @@ class Attributes implements IteratorAggregate, ArrayAccess } return $offset; } -} +} \ No newline at end of file diff --git a/src/Helpers/StringableEnumArray.php b/src/Helpers/StringableEnumArray.php new file mode 100644 index 0000000..9ef8931 --- /dev/null +++ b/src/Helpers/StringableEnumArray.php @@ -0,0 +1,49 @@ + + */ +class StringableEnumArray extends ArrayIterator implements Stringable +{ + /** + * @param array $array + */ + public function __construct( + $array = [], + protected string $separator = ', ' + ) { + parent::__construct($array); + } + + public function __toString() + { + return implode( + $this->separator, + array_filter( + $this->stringValues(), + fn($e) => !empty($e) + ) + ); + } + + /** + * @return array + */ + protected function stringValues(): array + { + return array_map( + function ($e) { + if ($e instanceof BackedEnum) $e = $e->value; + return strval($e); + }, + $this->getArrayCopy() + ); + } +} \ No newline at end of file diff --git a/src/Html5/ContentSectioningTags/AddressTag.php b/src/Html5/ContentSectioningTags/AddressTag.php index ade6185..64a4d05 100644 --- a/src/Html5/ContentSectioningTags/AddressTag.php +++ b/src/Html5/ContentSectioningTags/AddressTag.php @@ -6,6 +6,20 @@ use ByJoby\HTML\ContentCategories\FlowContent; use ByJoby\HTML\DisplayTypes\DisplayBlock; use ByJoby\HTML\Tags\AbstractContainerTag; +/** + * The
HTML element indicates that the enclosed HTML provides contact + * information for a person or people, or for an organization. + * + * The contact information provided by an
element's contents can take + * whatever form is appropriate for the context, and may include any type of + * contact information that is needed, such as a physical address, URL, email + * address, phone number, social media handle, geographic coordinates, and so + * forth. The
element should include the name of the person, people, + * or organization to which the contact information refers. + * + * Tag description by Mozilla Contributors licensed under CC-BY-SA 2.5 + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address + */ class AddressTag extends AbstractContainerTag implements DisplayBlock, FlowContent { const TAG = 'address'; diff --git a/src/Html5/ContentSectioningTags/ArticleTag.php b/src/Html5/ContentSectioningTags/ArticleTag.php index 8f552d7..6eddf5f 100644 --- a/src/Html5/ContentSectioningTags/ArticleTag.php +++ b/src/Html5/ContentSectioningTags/ArticleTag.php @@ -7,6 +7,17 @@ use ByJoby\HTML\ContentCategories\SectioningContent; use ByJoby\HTML\DisplayTypes\DisplayBlock; use ByJoby\HTML\Tags\AbstractContainerTag; +/** + * The
HTML element represents a self-contained composition in a + * document, page, application, or site, which is intended to be independently + * distributable or reusable (e.g., in syndication). Examples include: a forum + * post, a magazine or newspaper article, or a blog entry, a product card, a + * user-submitted comment, an interactive widget or gadget, or any other + * independent item of content. + * + * Tag description by Mozilla Contributors licensed under CC-BY-SA 2.5 + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article + */ class ArticleTag extends AbstractContainerTag implements DisplayBlock, FlowContent, SectioningContent { const TAG = 'article'; diff --git a/src/Html5/ContentSectioningTags/AsideTag.php b/src/Html5/ContentSectioningTags/AsideTag.php index 5fced0a..9f0cd1b 100644 --- a/src/Html5/ContentSectioningTags/AsideTag.php +++ b/src/Html5/ContentSectioningTags/AsideTag.php @@ -7,6 +7,14 @@ use ByJoby\HTML\ContentCategories\SectioningContent; use ByJoby\HTML\DisplayTypes\DisplayBlock; use ByJoby\HTML\Tags\AbstractContainerTag; +/** + * The