Skip to content

Commit ed081cb

Browse files
committed
feat: added merge specifications command, made sure specific enum choice-sets result in separate classes (eg: role, rel, type)
1 parent 6dc9468 commit ed081cb

152 files changed

Lines changed: 10043 additions & 399 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/ext-html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use Html\Command\BatchGeneratorCommand;
77
use Html\Command\CreateClassCommand;
88
use Html\Command\CreateEnumCommand;
99
use Html\Command\CreateJsonCommand;
10+
use Html\Command\MergeSpecifications;
1011
use Html\Command\WatchCommand;
1112
use Html\Service\ComponentBuilder;
1213
use Symfony\Component\Console\Input\InputInterface;
@@ -37,6 +38,10 @@ $app->command('generate:all [generator] [dest] [--overwrite-existing=]', functio
3738
(new BatchGeneratorCommand())($generator, $dest, $input, $output, (bool) $overwriteExisting);
3839
})->descriptions('Generates templates. Uses a given generator. Requires a Generator Name (eg. html, twig) and a destination path for the generated templates. You can also provide a list of generators separated by comma.', ['generator' => 'name of the generator(s)', 'dest' => 'destination path']);
3940

41+
$app->command('merge:specs [import] [dest]', function ($import, $dest, InputInterface $input, OutputInterface $output) {
42+
return (new MergeSpecifications())($import, $dest, $input, $output);
43+
})->descriptions('Merges custom framework or feature specifications into the HTML5 specifications and saves the result to a given destination file.', ['import' => 'path to the custom specifications file', 'dest' => 'path to the destination file']);
44+
4045
$sourceDefault = realpath(__DIR__ . '/../examples/template-generator/source');
4146
$destDefault = realpath(__DIR__ . '/../examples/template-generator/dest');
4247
$app

docs/phpmd.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ layout: home
1313
## Design
1414

1515

16-
Fri Oct 31 11:13:41 PM CET 2025
16+
Sat Nov 1 04:06:05 PM CET 2025

src/Command/CreateClassCommand.php

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,24 @@ private function generateEnumMethod(
304304
$details['elements'] = $elementsWithAttribute;
305305
}
306306

307-
if ($this->manyElementsHaveAttribute($attribute) && count($details['elements']) === 1) {
308-
$kebapCase .= ucfirst($element);
307+
// Check for element-specific enum first (e.g., ARoleEnum for anchor)
308+
// then fall back to generic enum (e.g., RoleEnum)
309+
$elementSpecificEnumName = ucfirst($element) . $kebapCase . 'Enum';
310+
$genericEnumName = $kebapCase . 'Enum';
311+
312+
// Check if element-specific enum file exists
313+
$elementSpecificPath = __DIR__ . '/../Enum/' . $elementSpecificEnumName . '.php';
314+
if (file_exists($elementSpecificPath)) {
315+
$enumName = $elementSpecificEnumName;
316+
} else {
317+
// Fall back to generic enum or old logic for single-element attributes
318+
if ($this->manyElementsHaveAttribute($attribute) && count($details['elements']) === 1) {
319+
$enumName = $kebapCase . ucfirst($element) . 'Enum';
320+
} else {
321+
$enumName = $genericEnumName;
322+
}
309323
}
310324

311-
$enumName = $kebapCase . 'Enum';
312325
$isUnionType = str_replace('enum', '', $type) !== '';
313326

314327
if ($isUnionType) {
@@ -439,14 +452,27 @@ private function processEnumAttribute(string $attribute, array $details, string
439452
$details['elements'] = $elementsWithAttribute;
440453
}
441454

442-
if ($this->manyElementsHaveAttribute($attribute) && count($details['elements']) === 1) {
443-
$kebapCase .= ucfirst($element);
455+
// Check for element-specific enum first (e.g., ARoleEnum for anchor)
456+
// then fall back to generic enum (e.g., RoleEnum)
457+
$elementSpecificEnumName = ucfirst($element) . $kebapCase . 'Enum';
458+
$genericEnumName = $kebapCase . 'Enum';
459+
460+
// Check if element-specific enum file exists
461+
$elementSpecificPath = __DIR__ . '/../Enum/' . $elementSpecificEnumName . '.php';
462+
if (file_exists($elementSpecificPath)) {
463+
$enumName = $elementSpecificEnumName;
464+
} else {
465+
// Fall back to generic enum or old logic for single-element attributes
466+
if ($this->manyElementsHaveAttribute($attribute) && count($details['elements']) === 1) {
467+
$enumName = $kebapCase . ucfirst($element) . 'Enum';
468+
} else {
469+
$enumName = $genericEnumName;
470+
}
444471
}
445472

446-
$this->uses[] = sprintf("Html\Enum\%sEnum", $kebapCase);
473+
$this->uses[] = sprintf("Html\Enum\%s", $enumName);
447474

448475
$isUnionType = str_replace('enum', '', $type) !== '';
449-
$enumName = $kebapCase . 'Enum';
450476

451477
if ($isUnionType) {
452478
$otherTypes = $this->getOtherTypesFromEnum($type);

src/Command/CreateEnumCommand.php

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,14 @@ public function __invoke(InputInterface $input, OutputInterface $output): int
7878
$cases = '';
7979
$className = ucfirst($element);
8080

81-
if ($this->manyElementsHaveAttribute($element) && count($attributes['elements']) === 1) {
81+
// If this is a generic enum (most common, >50% usage), don't prefix with element name
82+
if (isset($attributes['_is_generic']) && $attributes['_is_generic']) {
83+
// Keep generic name like "RoleEnum"
84+
$className = ucfirst($element);
85+
} elseif (isset($attributes['_element_specific']) && $attributes['_element_specific']) {
86+
// Element-specific enum for less common cases, prefix with element name
87+
$className = ucfirst($attributes['elements'][0]) . $className;
88+
} elseif ($this->manyElementsHaveAttribute($element) && count($attributes['elements']) === 1) {
8289
$className .= ucfirst($attributes['elements'][0]);
8390
}
8491

@@ -168,21 +175,95 @@ private function manyElementsHaveAttribute($attributeNAme): bool
168175
private function findEnumAttributes(): array
169176
{
170177
$enumAttributes = [];
171-
$i = 0;
172-
foreach ($this->data as $details) {
178+
$attributesByName = []; // Track all enum attributes by name
179+
180+
// First pass: collect all enum attributes grouped by attribute name
181+
foreach ($this->data as $elementName => $details) {
173182
if (isset($details['attributes'])) {
174183
foreach ($details['attributes'] as $attribute => $attributeDetails) {
175184
if (isset($attributeDetails['type'])) {
176185
$type = $attributeDetails['type'];
177186
// Support 'enum', 'enum|string', 'enum|boolean', etc.
178187
if ($type === 'enum' || (is_string($type) && preg_match('/(^|\|)enum($|\|)/', $type))) {
179-
$enumAttributes[$i][$attribute] = $attributeDetails;
188+
if (!isset($attributesByName[$attribute])) {
189+
$attributesByName[$attribute] = [];
190+
}
191+
$attributesByName[$attribute][] = [
192+
'element' => $elementName,
193+
'details' => $attributeDetails
194+
];
195+
}
196+
}
197+
}
198+
}
199+
}
200+
201+
// Second pass: determine if we need element-specific enums
202+
$i = 0;
203+
foreach ($attributesByName as $attributeName => $occurrences) {
204+
$choiceSets = [];
205+
$totalElements = count($occurrences);
206+
207+
// Group occurrences by their choice sets
208+
foreach ($occurrences as $occurrence) {
209+
$choices = $occurrence['details']['choices'] ?? [];
210+
sort($choices); // Sort for consistent comparison
211+
$choiceKey = implode('|', $choices);
212+
213+
if (!isset($choiceSets[$choiceKey])) {
214+
$choiceSets[$choiceKey] = [
215+
'choices' => $choices,
216+
'elements' => [],
217+
'details' => $occurrence['details']
218+
];
219+
}
220+
$choiceSets[$choiceKey]['elements'][] = $occurrence['element'];
221+
}
222+
223+
// If there's only one unique choice set, create a single shared enum
224+
if (count($choiceSets) === 1) {
225+
$choiceSet = reset($choiceSets);
226+
$enumAttributes[$i][$attributeName] = $choiceSet['details'];
227+
$enumAttributes[$i][$attributeName]['elements'] = $choiceSet['elements'];
228+
$i++;
229+
} else {
230+
// Multiple choice sets - find the most common one (>50% usage)
231+
$mostCommonChoiceSet = null;
232+
$mostCommonCount = 0;
233+
234+
foreach ($choiceSets as $choiceKey => $choiceSet) {
235+
$count = count($choiceSet['elements']);
236+
if ($count > $mostCommonCount) {
237+
$mostCommonCount = $count;
238+
$mostCommonChoiceSet = $choiceKey;
239+
}
240+
}
241+
242+
$threshold = $totalElements / 2;
243+
244+
// Create enums based on usage threshold
245+
foreach ($choiceSets as $choiceKey => $choiceSet) {
246+
$elementCount = count($choiceSet['elements']);
247+
248+
if ($choiceKey === $mostCommonChoiceSet && $elementCount > $threshold) {
249+
// This is the most common choice set and used by >50% - create generic enum
250+
$enumAttributes[$i][$attributeName] = $choiceSet['details'];
251+
$enumAttributes[$i][$attributeName]['elements'] = $choiceSet['elements'];
252+
$enumAttributes[$i][$attributeName]['_is_generic'] = true;
253+
$i++;
254+
} else {
255+
// Less common choice set - create element-specific enums
256+
foreach ($choiceSet['elements'] as $elementName) {
257+
$enumAttributes[$i][$attributeName] = $choiceSet['details'];
258+
$enumAttributes[$i][$attributeName]['elements'] = [$elementName];
259+
$enumAttributes[$i][$attributeName]['_element_specific'] = true;
180260
$i++;
181261
}
182262
}
183263
}
184264
}
185265
}
266+
186267
return $enumAttributes;
187268
}
188269

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Html\Command;
6+
7+
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Input\InputInterface;
9+
use Symfony\Component\Console\Output\OutputInterface;
10+
use Symfony\Component\Yaml\Yaml;
11+
12+
final class MergeSpecifications extends Command
13+
{
14+
private const HTML_DEFINITION_PATH = __DIR__ . \DIRECTORY_SEPARATOR . '..' . \DIRECTORY_SEPARATOR . 'Resources' . \DIRECTORY_SEPARATOR . 'specifications' . \DIRECTORY_SEPARATOR . 'html5.yaml';
15+
16+
public function __invoke(string $import, string $dest, InputInterface $input, OutputInterface $output): int
17+
{
18+
$customSpecs = Yaml::parseFile($import);
19+
$htmlSpecs = Yaml::parseFile(self::HTML_DEFINITION_PATH);
20+
$output = $htmlSpecs;
21+
22+
// Global CSS attributes that can be used on _ANY_ element
23+
if (array_key_exists('*', $customSpecs)) {
24+
foreach ($htmlSpecs as $element => $props) {
25+
if (!isset($output[$element]['attributes'])) {
26+
$output[$element]['attributes'] = $customSpecs['*']['attributes'];
27+
28+
continue;
29+
}
30+
$output[$element]['attributes'] = \array_merge_recursive($output[$element]['attributes'], $customSpecs['*']['attributes']);
31+
}
32+
}
33+
34+
// Regex matching
35+
foreach (array_keys($customSpecs) as $pattern) {
36+
if (@preg_match($pattern, '') !== false) {
37+
$keys = array_keys($output);
38+
$result = preg_grep($pattern, $keys);
39+
40+
foreach ($result as $key) {
41+
if (!isset($htmlSpecs[$key]['attributes'])) {
42+
$output[$key]['attributes'] = $customSpecs[$pattern]['attributes'];
43+
44+
continue;
45+
}
46+
$output[$key]['attributes'] = \array_merge_recursive($output[$key]['attributes'], $customSpecs[$pattern]['attributes']);
47+
}
48+
49+
unset($customSpecs[$pattern]); // Remove regex key from framework specs after processing (so we can deep merge all non-regex keys later)
50+
}
51+
}
52+
53+
// Deep merge everything else
54+
$output = \array_merge_recursive($output, $customSpecs);
55+
if (isset($output['*'])) {
56+
unset($output['*']); // Remove global wildcard key from output
57+
}
58+
\file_put_contents($dest, Yaml::dump($output, 10, 2));
59+
return Command::SUCCESS;
60+
}
61+
}

src/Element/Block/Article.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*
55
* Article - The article element represents a self-contained composition in a document, page, application, or site, which is intended to be independently distributable or reusable.
66
*
7-
* @generated 2025-10-31 22:22:33
7+
* @generated 2025-11-01 15:04:49
88
* @category HTML
99
* @package vardumper/extended-htmldocument
1010
* @subpackage Html\Element\Block

src/Element/Block/Aside.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*
55
* Aside - The aside element represents a section of a page that consists of content that is tangentially related to the content around the aside element, and which could be considered separate from that content.
66
*
7-
* @generated 2025-10-31 22:22:33
7+
* @generated 2025-11-01 15:04:49
88
* @category HTML
99
* @package vardumper/extended-htmldocument
1010
* @subpackage Html\Element\Block

src/Element/Block/Audio.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*
55
* Audio - The audio element is used to embed sound content in documents. It may contain one or more audio sources, represented using the src attribute or the source element.
66
*
7-
* @generated 2025-10-31 22:22:33
7+
* @generated 2025-11-01 15:04:49
88
* @category HTML
99
* @package vardumper/extended-htmldocument
1010
* @subpackage Html\Element\Block

src/Element/Block/Blockquote.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*
55
* Blockquote - The blockquote element represents a section that is quoted from another source. Content inside a blockquote must be quoted from another source, whose address, if it has one, may be cited in the cite attribute.
66
*
7-
* @generated 2025-10-31 22:22:33
7+
* @generated 2025-11-01 15:04:49
88
* @category HTML
99
* @package vardumper/extended-htmldocument
1010
* @subpackage Html\Element\Block

src/Element/Block/Body.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*
55
* Body - The body element represents the content of an HTML document. All the contents such as text, images, headings, links, tables, etc. are placed between the body tags.
66
*
7-
* @generated 2025-10-31 22:22:33
7+
* @generated 2025-11-01 15:04:49
88
* @category HTML
99
* @package vardumper/extended-htmldocument
1010
* @subpackage Html\Element\Block

0 commit comments

Comments
 (0)