Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContentModel
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 10
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDomElement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDomNode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initializeCategories
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getCategories
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 coalesceContentCategories
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 canAcceptContent
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 hasCategory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasAttribute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace pvc\html\content_model;
4
5use pvc\interfaces\html\content_model\ContentCategory;
6use pvc\interfaces\html\content_model\ContentModelInterface;
7use pvc\interfaces\html\content_model\ContentPermission;
8use pvc\interfaces\html\dom\DomElementInterface;
9use pvc\interfaces\html\dom\DomNodeInterface;
10use pvc\interfaces\html\rules\CategoryRuleInterface;
11use pvc\interfaces\html\rules\ContentRuleInterface;
12
13class ContentModel implements ContentModelInterface
14{
15    /**
16     * @var DomElementInterface
17     */
18    protected DomElementInterface $domElement;
19
20    /**
21     * @var DomNodeInterface
22     */
23    protected DomNodeInterface $domNode;
24
25    /**
26     * @var int
27     *  these are the categories to which this element belongs represented as a
28     * bitmask.  In other
29     *  words, if a parent node can only accept Transparent among its
30     *  children, then this int will need the Transparent bit flipped on
31     *  if it is to be permitted as a child.
32     */
33    protected(set) int $categories = 0;
34
35    /**
36     * @var int
37     * these are the categories dnoting acceptable content for this node.  In
38     * other words, if only Transparent is in this list, then the child must
39     * have the Transparent bit flipped on in order to be permitted
40     */
41    protected(set) int $childCategories = 0;
42
43    public function __construct(
44        /**
45         * @var array<ContentCategory> $initialCategories
46         * categories to which this model always belongs, e.g. before applying
47         * context-sensitive context rules which can add categories
48         */
49        private array $initialCategories,
50
51        /**
52         * @var array<CategoryRuleInterface> $additionalCategoryRules
53         * rules that can add additional categories to the $contentCategories
54         * array based on contextual conditions.  The test method of each rule is
55         * invoked and produces an array which is merged with the initialCategories
56         * array
57         */
58        private array $additionalCategoryRules,
59
60        /**
61         * @var array<ContentCategory> $content
62         * this array indicates what sorts of content categories are permitted
63         * as children of this node.
64         */
65        protected(set) array $content,
66
67        /**
68         * @var array<ContentRuleInterface> $contentRules
69         * additional rules that might permit or deny a content model to be a child
70         * of this one
71         */
72        private(set) array $contentRules,
73    ) {
74    }
75
76    public function getDomElement(): DomElementInterface
77    {
78        return $this->domElement;
79    }
80
81    public function getDomNode(): DomNodeInterface
82    {
83        return $this->domNode;
84    }
85
86    public function initializeCategories(): void
87    {
88        $derivedCategories = [];
89        foreach ($this->additionalCategoryRules as $rule) {
90            $derivedCategories += $rule->test($this->getDomNode());
91        }
92        /**
93         * merge initial and derived, and coalesce to an int
94         */
95        $allCategories = self::coalesceContentCategories(array_merge($this->initialCategories, $derivedCategories));
96
97        /**
98         * convert permitted content categories to bitmask
99         */
100        $this->childCategories = self::coalesceContentCategories($this->content);
101    }
102
103    public function getCategories(): int
104    {
105        return $this->categories;
106    }
107
108    /**
109     * @param  array<ContentCategory>  $categories
110     *
111     * @return int
112     * callback to create a bitmask of the content categories to which this model belongs
113     */
114    public static function coalesceContentCategories(array $categories): int
115    {
116        /**
117         * convert categories to integers
118         */
119        $categories = array_map(fn(ContentCategory $item) => $item->value, $categories);
120
121        /**
122         * @param  int  $carry
123         * @param  int  $category
124         *
125         * @return int
126         */
127
128        $reducer = function(int $carry, int $category) {
129            return $carry + $category;
130        };
131
132        /**
133         * make sure the categories are unique
134         */
135        return array_reduce(array_unique($categories), $reducer, 0);
136    }
137
138    public function canAcceptContent(DomNodeInterface $content): bool
139    {
140        /**
141         * first iterate through the content rules.
142         */
143        foreach ($this->contentRules as $rule) {
144            if ($rule->test($content) === ContentPermission::GRANTED) return true;
145            if ($rule->test($content) === ContentPermission::DENIED) return false;
146        }
147
148        /**
149         * if the intersection of this model's content categories and the child's
150         * categories is > 0, return true else return false.  This test could
151         * be a rule itself, but then you would have to insert this rule into
152         * the list of content rules for every element, which seems quite redundant.
153         * So the logic is kept in this method instead.
154         */
155        return (($this->childCategories & $this->getCategories()) > 0);
156    }
157
158    /**
159     * the following support methods are used to define the characteristics
160     * or context of the containing element
161     */
162
163    /**
164     * @param  ContentCategory  $category
165     *
166     * @return bool
167     */
168    public function hasCategory(ContentCategory $category): bool
169    {
170        return (0 < ($this->categories & $category->value));
171    }
172
173    public function hasAttribute(string $attributeName): bool
174    {
175        return $this->domElement->getAttribute($attributeName) !== null;
176    }
177
178    public function hasName(string $name): bool
179    {
180        return $this->domElement->getName() === $name;
181    }
182}