Browse Source

Moved MagicProperties and Exception to framework repository

wrapper-classes
Dustin Wilson 3 years ago
parent
commit
e23041a6c3
  1. 117
      RoboFile.php
  2. 3
      composer.json
  3. 38
      composer.lock
  4. 72
      lib/DOMException.php
  5. 1
      lib/Document.php
  6. 1
      lib/DocumentFragment.php
  7. 1
      lib/Element.php
  8. 59
      lib/Exception.php
  9. 2
      lib/TokenList.php
  10. 79
      lib/traits/MagicProperties.php
  11. 17
      postcss.config.js
  12. 1
      tests/cases/TestDocument.php
  13. 49
      tests/cases/TestException.php
  14. 79
      tests/cases/TestMagicProperties.php
  15. 2
      tests/phpunit.dist.xml

117
RoboFile.php

@ -147,119 +147,4 @@ class RoboFile extends \Robo\Tasks {
}
return $this->taskExec($executor)->option("-d", "zend.assertions=1")->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run();
}
/** Runs the coding standards fixer */
public function clean($opts = ['demo|d' => false]): Result {
$t = $this->taskExec(norm(BASE."vendor/bin/php-cs-fixer"));
$t->arg("fix");
if ($opts['demo']) {
$t->args("--dry-run", "--diff")->option("--diff-format", "udiff");
}
return $t->run();
}
/** Produces the CharacterReference class file */
public function charref() {
$template = <<<'FILE'
<?php
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
// This file is machine-generated
// DO NOT MODIFY
// To update, run ./robo charref
class CharacterReference {
const LONGEST_NAME = %LONGEST%;
const PREFIX_PATTERN = %NAMED_PATTERN%;
const NAMES = [
%NAMED_REFERENCES%
];
const C1_TABLE = [
%C1_SUBSTITUTIONS%
];
}
FILE;
$input = @json_decode(@file_get_contents("https://html.spec.whatwg.org/entities.json"), true);
if (!is_array($input)) {
throw new \Exception("Could not retrieve character reference table.");
}
$list = [];
$terms = [];
foreach ($input as $entity => $data) {
// strip the ampersand from the entity name
$entity = substr($entity, 1);
// add the entity name to an array of regular expression terms
// if the entry exists in unterminated form, compress it into one, skiping the unterminated version
if (substr($entity, -1) === ';') {
if (isset($input['&'.substr($entity, 0, strlen($entity) -1)])) {
$terms[] = "$entity?";
} else {
$terms[] = $entity;
}
}
// add a PHP-code representation of the entity name and its characters to another array
$chars = $data['codepoints'];
for ($a = 0; $a < sizeof($chars); $a++) {
$chars[$a] = '\u{'.dechex($chars[$a]).'}';
}
$chars = implode('', $chars);
$list[] = "'$entity'=>\"$chars\"";
}
// concatenate the list of entities and substitute them into the template
$list = implode(",", $list);
$template = str_replace('%NAMED_REFERENCES%', $list, $template);
// prepare the list of terms as a regular expression
// sort longest terms first
usort($terms, function($a, $b) {
return -1 * (strlen(preg_replace("/\W/", "", $a)) <=> strlen(preg_replace("/\W/", "", $b)));
});
// note the longest term
$longest = strlen(preg_replace("/\W/", "", $terms[0]));
$template = str_replace('%LONGEST%', $longest, $template);
// concatenate the terms into a case-sensitive non-capturing prefix search
$regexp = '/^(?:'.implode('|', $terms).')/';
$template = str_replace('%NAMED_PATTERN%', var_export($regexp, true), $template);
// Compile the C1 control substitution table
// See https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
$list = [];
$c1table = [
0x80 => 0x20AC, // EURO SIGN (€)
0x82 => 0x201A, // SINGLE LOW-9 QUOTATION MARK (‚)
0x83 => 0x0192, // LATIN SMALL LETTER F WITH HOOK (ƒ)
0x84 => 0x201E, // DOUBLE LOW-9 QUOTATION MARK („)
0x85 => 0x2026, // HORIZONTAL ELLIPSIS (…)
0x86 => 0x2020, // DAGGER (†)
0x87 => 0x2021, // DOUBLE DAGGER (‡)
0x88 => 0x02C6, // MODIFIER LETTER CIRCUMFLEX ACCENT (ˆ)
0x89 => 0x2030, // PER MILLE SIGN (‰)
0x8A => 0x0160, // LATIN CAPITAL LETTER S WITH CARON (Š)
0x8B => 0x2039, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK (‹)
0x8C => 0x0152, // LATIN CAPITAL LIGATURE OE (Œ)
0x8E => 0x017D, // LATIN CAPITAL LETTER Z WITH CARON (Ž)
0x91 => 0x2018, // LEFT SINGLE QUOTATION MARK (‘)
0x92 => 0x2019, // RIGHT SINGLE QUOTATION MARK (’)
0x93 => 0x201C, // LEFT DOUBLE QUOTATION MARK (“)
0x94 => 0x201D, // RIGHT DOUBLE QUOTATION MARK (”)
0x95 => 0x2022, // BULLET (•)
0x96 => 0x2013, // EN DASH (–)
0x97 => 0x2014, // EM DASH (—)
0x98 => 0x02DC, // SMALL TILDE (˜)
0x99 => 0x2122, // TRADE MARK SIGN (™)
0x9A => 0x0161, // LATIN SMALL LETTER S WITH CARON (š)
0x9B => 0x203A, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (›)
0x9C => 0x0153, // LATIN SMALL LIGATURE OE (œ)
0x9E => 0x017E, // LATIN SMALL LETTER Z WITH CARON (ž)
0x9F => 0x0178, // LATIN CAPITAL LETTER Y WITH DIAERESIS (Ÿ)
];
foreach ($c1table as $c1 => $code) {
$list[] = "$c1=>$code";
}
$list = implode(",", $list);
$template = str_replace('%C1_SUBSTITUTIONS%', $list, $template);
// output the file itself
file_put_contents(BASE."lib/CharacterReference.php", $template);
}
}
}

3
composer.json

@ -5,7 +5,8 @@
"require": {
"php": ">=7.4",
"ext-dom": "*",
"mensbeam/html-parser": "dev-master"
"mensbeam/html-parser": "dev-master",
"mensbeam/framework": "^1.0"
},
"scripts": {
"post-install-cmd": ["@composer bin all install"],

38
composer.lock

@ -4,8 +4,44 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3371a9b669d02688e062be96d83f1eff",
"content-hash": "fb8d8d217fab6f78a9bdf736c8da2316",
"packages": [
{
"name": "mensbeam/framework",
"version": "1.0",
"source": {
"type": "git",
"url": "https://code.mensbeam.com/MensBeam/Framework",
"reference": "503ade585f25a740d2f2f5b63b8c1a4ac6820cea"
},
"require": {
"php": ">=7.4"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"MensBeam\\Framework\\": [
"lib/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dustin Wilson",
"email": "dustin@dustinwilson.com",
"homepage": "https://dustinwilson.com/"
}
],
"description": "Common classes and traits used in many Mensbeam projects",
"time": "2021-10-11T14:53:50+00:00"
},
{
"name": "mensbeam/html-parser",
"version": "dev-master",

72
lib/DOMException.php

@ -7,8 +7,10 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\Exception;
class DOMException extends \Exception {
class DOMException extends Exception {
// From PHP's DOMException; keeping error codes consistent
const HIERARCHY_REQUEST_ERROR = 3;
const WRONG_DOCUMENT = 4;
@ -22,58 +24,26 @@ class DOMException extends \Exception {
const INVALID_ACCESS_ERROR = 15;
const VALIDATION_ERROR = 16;
const OUTER_HTML_FAILED_NOPARENT = 101;
protected static $messages = [
3 => 'Hierarchy request error; supplied node is not allowed here',
4 => 'Supplied node does not belong to this document',
5 => 'Invalid character',
7 => 'Modification not allowed here',
8 => 'Not found error',
9 => 'Feature is not supported because %s',
12 => 'Syntax error',
13 => 'Invalid modification error',
14 => 'Namespace error',
15 => 'Invalid access error',
16 => 'Validation error',
const OUTER_HTML_FAILED_NOPARENT = 301;
101 => 'Failed to set the "outerHTML" property; the element does not have a parent node'
];
public function __construct(int $code, ...$args) {
if (!isset(self::$messages[$code])) {
throw new Exception(Exception::INVALID_CODE);
}
$message = self::$messages[$code];
$previous = null;
// @codeCoverageIgnoreStart
if ($args) {
// Grab a previous exception if there is one.
if ($args[0] instanceof \Throwable) {
$previous = array_shift($args);
} elseif (end($args) instanceof \Throwable) {
$previous = array_pop($args);
}
}
// @codeCoverageIgnoreEnd
// Count the number of replacements needed in the message.
preg_match_all('/(\%(?:\d+\$)?s)/', $message, $matches);
$count = count($matches[1]);
// If the number of replacements don't match the arguments then oops.
if (count($args) !== $count) {
throw new Exception(Exception::INCORRECT_PARAMETERS_FOR_MESSAGE, $count);
}
if ($count > 0) {
// Go through each of the arguments and run sprintf on the strings.
$message = call_user_func_array('sprintf', array_merge([$message], $args));
}
parent::__construct($message, $code, $previous);
self::$messages = array_replace(parent::$messages, [
3 => 'Hierarchy request error; supplied node is not allowed here',
4 => 'Supplied node does not belong to this document',
5 => 'Invalid character',
7 => 'Modification not allowed here',
8 => 'Not found error',
9 => 'Feature is not supported because %s',
12 => 'Syntax error',
13 => 'Invalid modification error',
14 => 'Namespace error',
15 => 'Invalid access error',
16 => 'Validation error',
301 => 'Failed to set the "outerHTML" property; the element does not have a parent node'
]);
parent::__construct($code, ...$args);
}
}

1
lib/Document.php

@ -7,6 +7,7 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\MagicProperties;
use MensBeam\HTML\Parser,
MensBeam\HTML\Parser\Data;

1
lib/DocumentFragment.php

@ -7,6 +7,7 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\MagicProperties;
class DocumentFragment extends \DOMDocumentFragment {

1
lib/Element.php

@ -7,6 +7,7 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\MagicProperties;
use MensBeam\HTML\Parser;

59
lib/Exception.php

@ -7,62 +7,7 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\Exception as FrameworkException;
class Exception extends \Exception {
const INVALID_CODE = 100;
const UNKNOWN_ERROR = 101;
const INCORRECT_PARAMETERS_FOR_MESSAGE = 102;
const UNREACHABLE_CODE = 103;
const NONEXISTENT_PROPERTY = 201;
const READONLY_PROPERTY = 202;
const ARGUMENT_TYPE_ERROR = 203;
protected static $messages = [
100 => 'Invalid error code',
101 => 'Unknown error; escaping',
102 => 'Incorrect number of parameters for Exception message; %s expected',
103 => 'Unreachable code',
201 => 'Property %s does not exist',
202 => 'Cannot write readonly property %s',
203 => 'Argument #%s ($%s) must be of type %s, %s given'
];
public function __construct(int $code, ...$args) {
if (!isset(self::$messages[$code])) {
throw new Exception(Exception::INVALID_CODE);
}
$message = self::$messages[$code];
$previous = null;
// @codeCoverageIgnoreStart
if ($args) {
// Grab a previous exception if there is one.
if ($args[0] instanceof \Throwable) {
$previous = array_shift($args);
} elseif (end($args) instanceof \Throwable) {
$previous = array_pop($args);
}
}
// @codeCoverageIgnoreEnd
// Count the number of replacements needed in the message.
preg_match_all('/(\%(?:\d+\$)?s)/', $message, $matches);
$count = count($matches[1]);
// If the number of replacements don't match the arguments then oops.
if (count($args) !== $count) {
throw new Exception(Exception::INCORRECT_PARAMETERS_FOR_MESSAGE, $count);
}
if ($count > 0) {
// Go through each of the arguments and run sprintf on the strings.
$message = call_user_func_array('sprintf', array_merge([$message], $args));
}
parent::__construct($message, $code, $previous);
}
}
class Exception extends FrameworkException {}

2
lib/TokenList.php

@ -7,6 +7,8 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\MagicProperties;
class TokenList implements \ArrayAccess, \Countable, \Iterator {
use MagicProperties;

79
lib/traits/MagicProperties.php

@ -1,79 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2017, Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
/**
* Getters and setters in PHP sucks. Instead of having getter and setter
* function types for classes we instead have the __get and __set magic methods
* to handle all properties. Not only are they unwieldy to use when you have
* many properties they also become difficult to handle when inheriting where
* traits are involved. This trait attempts to create hackish getter and setter
* functions that can be extended by simple inheritance.
*/
trait MagicProperties {
public function __get(string $name) {
$methodName = $this->getMagicPropertyMethodName($name);
if ($methodName === null) {
throw new Exception(Exception::NONEXISTENT_PROPERTY, $name);
}
return call_user_func([ $this, $methodName ]);
}
public function __isset(string $name): bool {
return ($this->getMagicPropertyMethodName($name) !== null);
}
public function __set(string $name, $value) {
$methodName = $this->getMagicPropertyMethodName($name, false);
if ($methodName !== null) {
call_user_func([ $this, $methodName ], $value);
return;
}
if ($this->getMagicPropertyMethodName($name) !== null) {
throw new Exception(Exception::READONLY_PROPERTY, $name);
} else {
throw new Exception(Exception::NONEXISTENT_PROPERTY, $name);
}
}
public function __unset(string $name) {
$methodName = $this->getMagicPropertyMethodName($name, false);
if ($methodName === null) {
throw new Exception(Exception::READONLY_PROPERTY, $name);
}
call_user_func([ $this, $methodName ], null);
}
// Method_exists is case-insensitive because methods are case-insensitive in
// PHP. Properties in PHP 8 are sensitive, so let's use reflection to check
// against the actual name to get a case sensitive match like methods should be!
private function getMagicPropertyMethodName(string $name, bool $get = true): ?string {
static $protectedMethodsList = null;
$methodName = "__" . (($get) ? 'get' : 'set') . "_{$name}";
if (method_exists($this, $methodName)) {
if ($protectedMethodsList === null) {
$reflector = new \ReflectionClass($this);
// Magic property methods are protected
$protectedMethodsList = $reflector->getMethods(\ReflectionMethod::IS_PROTECTED);
}
foreach ($protectedMethodsList as $method) {
if ($method->name === $methodName) {
return $methodName;
}
}
}
return null;
}
}

17
postcss.config.js

@ -1,17 +0,0 @@
module.exports = ctx => ({
//map: ctx.options.map,
parser: 'postcss-scss',
//syntax: 'postcss-scss',
plugins: {
'postcss-import': { root: ctx.file.dirname },
'postcss-discard-comments': {},
'postcss-sassy-mixins': {},
'postcss-custom-media': {preserve: false},
'postcss-media-minmax': {},
'postcss-custom-properties': {preserve: false},
'postcss-color-function': {},
'postcss-nested': {},
'autoprefixer': {},
'postcss-csso': {},
}
})

1
tests/cases/TestDocument.php

@ -185,7 +185,6 @@ class TestDocument extends \PHPUnit\Framework\TestCase {
/**
* @dataProvider provideDocumentCreationFailures
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Exception::__construct
*/
public function testDocumentCreationFailures($source): void {
$this->expectException(Exception::class);

49
tests/cases/TestException.php

@ -1,49 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2017, Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\HTML\DOM\{
DOMException,
Exception
};
/**
* @covers \MensBeam\HTML\DOM\DOMException
* @covers \MensBeam\HTML\DOM\Exception
*/
class TestException extends \PHPUnit\Framework\TestCase {
public function provideConstructorFailures(): iterable {
return [
[ function() {
$d = new DOMException(2112);
}, Exception::INVALID_CODE ],
[ function() {
$d = new Exception(2112);
}, Exception::INVALID_CODE ],
[ function() {
throw new DOMException(DOMException::NOT_FOUND, 'FAIL');
}, Exception::INCORRECT_PARAMETERS_FOR_MESSAGE ],
[ function() {
throw new Exception(Exception::UNKNOWN_ERROR, 'FAIL');
}, Exception::INCORRECT_PARAMETERS_FOR_MESSAGE ],
];
}
/**
* @dataProvider provideConstructorFailures
* @covers \MensBeam\HTML\DOM\DOMException::__construct
* @covers \MensBeam\HTML\DOM\Exception::__construct
*/
public function testConstructorFailures(\Closure $closure, int $errorCode): void {
$this->expectException(Exception::class);
$this->expectExceptionCode($errorCode);
$closure();
}
}

79
tests/cases/TestMagicProperties.php

@ -1,79 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2017, Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\HTML\DOM\{
Document,
Exception,
MagicProperties
};
/** @covers \MensBeam\HTML\DOM\MagicProperties */
class TestMagicProperties extends \PHPUnit\Framework\TestCase {
public function provideFailures(): iterable {
return [
[ function() {
$d = new Document();
$d->omgWTFBBQ;
}, Exception::NONEXISTENT_PROPERTY ],
[ function() {
$d = new Document();
$d->omgWTFBBQ = 'ook';
}, Exception::NONEXISTENT_PROPERTY ],
[ function() {
$d = new Document();
$d->xpath = 'ook';
}, Exception::READONLY_PROPERTY ],
[ function() {
$d = new Document();
unset($d->xpath);
}, Exception::READONLY_PROPERTY ]
];
}
/**
* @dataProvider provideFailures
* @covers \MensBeam\HTML\DOM\MagicProperties::__get
* @covers \MensBeam\HTML\DOM\MagicProperties::__set
* @covers \MensBeam\HTML\DOM\MagicProperties::__unset
*/
public function testFailures(\Closure $closure, int $errorCode): void {
$this->expectException(Exception::class);
$this->expectExceptionCode($errorCode);
$closure();
}
/** @covers \MensBeam\HTML\DOM\MagicProperties::__isset */
public function testIsset(): void {
$d = new Document();
$this->assertTrue(isset($d->body));
}
/** @covers \MensBeam\HTML\DOM\MagicProperties::__unset */
public function testUnset(): void {
// Nothing allows setting values to null yet, so make one
$d = new class {
use MagicProperties;
protected ?string $_ook = 'ook';
protected function __get_ook(): ?string {
return $this->_ook;
}
protected function __set_ook(?string $value): void {
$this->_ook = $value;
}
};
unset($d->ook);
$this->assertNull($d->ook);
}
}

2
tests/phpunit.dist.xml

@ -22,9 +22,7 @@
<file>cases/TestDocumentOrElement.php</file>
<file>cases/TestElement.php</file>
<file>cases/TestElementMap.php</file>
<file>cases/TestException.php</file>
<file>cases/TestLeafNode.php</file>
<file>cases/TestMagicProperties.php</file>
<file>cases/TestMoonwalk.php</file>
<file>cases/TestNode.php</file>
<file>cases/TestParentNode.php</file>

Loading…
Cancel
Save