Skip to content

Commit d51556c

Browse files
committed
chore: add specification parameter to composed command, allow generator array
1 parent 13c94c8 commit d51556c

378 files changed

Lines changed: 6331 additions & 4472 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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ $app->command('generate:all [generator] [dest] [--overwrite-existing=] [--specif
3939
(new BatchGeneratorCommand())($generator, $dest, $input, $output, (bool) $overwriteExisting, $specification);
4040
})->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']);
4141

42-
$app->command('generate:composed [generator] [dest] [--overwrite-existing=]', function ($generator, $dest, InputInterface $input, OutputInterface $output, $overwriteExisting = false) {
43-
return (new GenerateComposedCommand())($generator, $dest, $input, $output, (bool) $overwriteExisting);
42+
$app->command('generate:composed [generator] [dest] [--overwrite-existing=] [--specification=]', function ($generator, $dest, InputInterface $input, OutputInterface $output, $overwriteExisting = false, $specification = null) {
43+
return (new GenerateComposedCommand())($generator, $dest, $input, $output, (bool) $overwriteExisting, $specification);
4444
})->descriptions('Generates composed component templates showing valid parent-child relationships based on content model metadata', ['generator' => 'name of the generator (nextjs, storybook, twig)', 'dest' => 'destination path']);
4545

4646
$app->command('merge:specs [import] [dest]', function ($import, $dest, InputInterface $input, OutputInterface $output) {

clover.xml

Lines changed: 2266 additions & 354 deletions
Large diffs are not rendered by default.

docs/code-generation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ php vendor/bin/ext-html make:classes a # only generates the Html\Element\Anchor
2525
php vendor/bin/ext-html make:classes # generates all classes, no enums
2626
php vendor/bin/ext-html make:all # generates both enums and classes
2727
php vendor/bin/ext-html generate:all twig,storybook templates # generates all twig templates and storybook stories, and saved them into templates folder
28+
php vendor/bin/ext-html generate:composed twig,storybook templates # generates composed templates based on content model (eg <table> with <tr>, <td>, etc), and saves them into templates folder
2829
php vendor/bin/ext-html merge:specs custom/schema.yaml destination/path.yaml # merges a custom regex or element-based specification into the html5.yaml file
2930
php vendor/bin/ext-html watch source/path dest/path # component builder. watches for component yaml changes, and generates templates on the fly
3031
```

src/Command/BatchGeneratorCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class BatchGeneratorCommand extends Command
2727
use ClassResolverTrait;
2828
use GeneratorResolverTrait;
2929

30-
private const HTML_DEFINITION_PATH = __DIR__ . '/../Resources/specifications/html5.yaml';
30+
private const HTML_DEFINITION_PATH = __DIR__ . '/../Resources/specifications/html5-with-aria.yaml';
3131

3232
private ?array $data = null;
3333
private SymfonyStyle $io;

src/Command/GenerateComposedCommand.php

Lines changed: 100 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
use Html\TemplateGenerator\StorybookJSGenerator;
1313
use Html\TemplateGenerator\TwigGenerator;
1414
use Html\Trait\ClassResolverTrait;
15+
use Html\Trait\GeneratorResolverTrait;
1516
use ReflectionClass;
1617
use Symfony\Component\Console\Command\Command;
1718
use Symfony\Component\Console\Input\InputArgument;
1819
use Symfony\Component\Console\Input\InputInterface;
1920
use Symfony\Component\Console\Input\InputOption;
2021
use Symfony\Component\Console\Output\OutputInterface;
2122
use Symfony\Component\Console\Style\SymfonyStyle;
23+
use Symfony\Component\Yaml\Yaml;
2224

2325
/**
2426
* Generate composed component templates showing valid parent-child relationships
@@ -30,13 +32,19 @@
3032
class GenerateComposedCommand extends Command
3133
{
3234
use ClassResolverTrait;
35+
use GeneratorResolverTrait;
36+
37+
private const HTML_DEFINITION_PATH = __DIR__ . '/../Resources/specifications/html5-with-aria.yaml';
38+
39+
private ?array $data = null;
3340

3441
public function __invoke(
3542
string $generator,
3643
string $dest,
3744
InputInterface $input,
3845
OutputInterface $output,
39-
bool $overwriteExisting = false
46+
bool $overwriteExisting = false,
47+
?string $specification = null
4048
): int {
4149
$io = new SymfonyStyle($input, $output);
4250

@@ -45,74 +53,88 @@ public function __invoke(
4553
return Command::FAILURE;
4654
}
4755

48-
// Instantiate the appropriate generator
49-
$generatorInstance = match ($generator) {
50-
'nextjs' => new NextJSGenerator(),
51-
'storybook' => new StorybookJSGenerator(),
52-
'twig' => new TwigGenerator(),
53-
default => null,
54-
};
55-
56-
if ($generatorInstance === null) {
57-
$io->error("Unsupported generator '{$generator}'. Supported generators: nextjs, storybook, twig");
58-
return Command::FAILURE;
56+
if (! \str_contains($generator, ',')) {
57+
$generators = [$generator];
58+
} else {
59+
$generators = \explode(',', $generator);
5960
}
6061

61-
// Check if generator supports composed element rendering
62-
if (!method_exists($generatorInstance, 'renderComposedElement')) {
63-
$io->error("Generator '{$generator}' does not support composed element rendering.");
62+
$specificationPath = $input->getOption('specification');
63+
if (! $this->loadHtmlDefinitions($specificationPath)) {
6464
return Command::FAILURE;
6565
}
66-
67-
$baseClasses = [InlineElement::class, BlockElement::class, VoidElement::class];
68-
$elements = [];
69-
foreach ($baseClasses as $baseClass) {
70-
$elements = array_merge($elements, $this->getClassesExtendingClass($baseClass));
71-
}
72-
73-
$dom = HTMLDocumentDelegator::createEmpty();
74-
$generatedCount = 0;
75-
$skippedCount = 0;
76-
77-
// Create content directory
78-
$contentDir = rtrim($dest, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR . $generator . \DIRECTORY_SEPARATOR . 'composed';
79-
if (! is_dir($contentDir)) {
80-
mkdir($contentDir, 0755, true);
81-
}
82-
83-
foreach ($elements as $className) {
84-
/** @var class-string<\Html\Interface\HTMLElementInterface> $className */
85-
$elementInstance = $className::create($dom);
86-
87-
// Check if element has SPECIFIC child requirements (not empty $parentOf)
88-
// Skip elements that can contain any content
89-
$ref = new ReflectionClass($className);
90-
$parentOf = $ref->getStaticPropertyValue('parentOf', []);
91-
92-
if (empty($parentOf)) {
93-
$skippedCount++;
94-
continue;
66+
// // Instantiate the appropriate generator
67+
// $generatorInstance = match ($generator) {
68+
// 'nextjs' => new NextJSGenerator(),
69+
// 'storybook' => new StorybookJSGenerator(),
70+
// 'twig' => new TwigGenerator(),
71+
// default => null,
72+
// };
73+
74+
// if ($generatorInstance === null) {
75+
// $io->error("Unsupported generator '{$generator}'. Supported generators: nextjs, storybook, twig");
76+
// return Command::FAILURE;
77+
// }
78+
79+
$templateGenerators = $this->getGenerators($generators);
80+
foreach ($templateGenerators as $name => $generatorInstance) {
81+
82+
// Check if generator supports composed element rendering
83+
if (!method_exists($generatorInstance, 'renderComposedElement')) {
84+
$io->error("Generator '{$generator}' does not support composed element rendering.");
85+
return Command::FAILURE;
9586
}
9687

97-
$output = $generatorInstance->renderComposedElement($elementInstance);
98-
if ($output === null) {
99-
$skippedCount++;
100-
continue;
88+
$baseClasses = [InlineElement::class, BlockElement::class, VoidElement::class];
89+
$elements = [];
90+
foreach ($baseClasses as $baseClass) {
91+
$elements = array_merge($elements, $this->getClassesExtendingClass($baseClass));
10192
}
10293

103-
$elementShortName = $ref->getShortName();
104-
$fileName = $elementInstance::QUALIFIED_NAME . '.composed.' . $generatorInstance->getExtension();
105-
$outFile = $contentDir . \DIRECTORY_SEPARATOR . $fileName;
94+
$dom = HTMLDocumentDelegator::createEmpty();
95+
$generatedCount = 0;
96+
$skippedCount = 0;
10697

107-
if (file_exists($outFile) && ! $overwriteExisting) {
108-
$io->warning("File '{$outFile}' already exists. Skipping.");
109-
$skippedCount++;
110-
continue;
98+
// Create content directory
99+
$contentDir = rtrim($dest, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR . $generator . \DIRECTORY_SEPARATOR . 'composed';
100+
if (! is_dir($contentDir)) {
101+
mkdir($contentDir, 0755, true);
111102
}
112103

113-
file_put_contents($outFile, $output);
114-
$io->success("Generated: {$outFile}");
115-
$generatedCount++;
104+
foreach ($elements as $className) {
105+
/** @var class-string<\Html\Interface\HTMLElementInterface> $className */
106+
$elementInstance = $className::create($dom);
107+
108+
// Check if element has SPECIFIC child requirements (not empty $parentOf)
109+
// Skip elements that can contain any content
110+
$ref = new ReflectionClass($className);
111+
$parentOf = $ref->getStaticPropertyValue('parentOf', []);
112+
113+
if (empty($parentOf)) {
114+
$skippedCount++;
115+
continue;
116+
}
117+
118+
$output = $generatorInstance->renderComposedElement($elementInstance);
119+
if ($output === null) {
120+
$skippedCount++;
121+
continue;
122+
}
123+
124+
$elementShortName = $ref->getShortName();
125+
$fileName = $elementInstance::QUALIFIED_NAME . '.composed.' . $generatorInstance->getExtension();
126+
$outFile = $contentDir . \DIRECTORY_SEPARATOR . $fileName;
127+
128+
if (file_exists($outFile) && ! $overwriteExisting) {
129+
$io->warning("File '{$outFile}' already exists. Skipping.");
130+
$skippedCount++;
131+
continue;
132+
}
133+
134+
file_put_contents($outFile, $output);
135+
$io->success("Generated: {$outFile}");
136+
$generatedCount++;
137+
}
116138
}
117139

118140
$io->success("Generated {$generatedCount} composed templates (specific composition patterns only). Skipped {$skippedCount} elements.");
@@ -142,4 +164,24 @@ public function configure(): void
142164
false
143165
);
144166
}
167+
168+
private function loadHtmlDefinitions(?string $specificationPath): bool
169+
{
170+
if ($specificationPath !== null) {
171+
if (! is_file($specificationPath)) {
172+
$this->io->error('Specification file not found at ' . $specificationPath);
173+
return false;
174+
}
175+
$this->data = Yaml::parseFile($specificationPath);
176+
return true;
177+
}
178+
179+
if (! is_file(self::HTML_DEFINITION_PATH)) {
180+
$this->io->error('HTML definition file not found.');
181+
return false;
182+
}
183+
184+
$this->data = Yaml::parseFile(self::HTML_DEFINITION_PATH);
185+
return true;
186+
}
145187
}

src/Element/Block/Article.php

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,49 @@
11
<?php
2-
32
/**
43
* This file is auto-generated. Do not edit manually.
54
*
65
* 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.
7-
*
8-
* @generated 2025-11-02 22:39:29
6+
*
7+
* @generated 2025-11-05 11:58:47
8+
* @category HTML
9+
* @package vardumper/extended-htmldocument
910
* @subpackage Html\Element\Block
1011
* @link https://vardumper.github.io/extended-htmldocument/
1112
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article
1213
*/
13-
1414
namespace Html\Element\Block;
1515

1616
use Html\Element\BlockElement;
17+
use Html\Element\Block\Aside;
18+
use Html\Element\Block\Audio;
19+
use Html\Element\Block\Blockquote;
20+
use Html\Element\Block\Body;
21+
use Html\Element\Block\DefinitionList;
22+
use Html\Element\Block\DeletedText;
23+
use Html\Element\Block\Division;
24+
use Html\Element\Block\Embed;
25+
use Html\Element\Block\Figure;
26+
use Html\Element\Block\Form;
27+
use Html\Element\Block\Heading1;
28+
use Html\Element\Block\Heading2;
29+
use Html\Element\Block\Heading3;
30+
use Html\Element\Block\Heading4;
31+
use Html\Element\Block\Heading5;
32+
use Html\Element\Block\Heading6;
33+
use Html\Element\Block\InlineFrame;
34+
use Html\Element\Block\InsertedText;
35+
use Html\Element\Block\Main;
36+
use Html\Element\Block\Map;
37+
use Html\Element\Block\Navigation;
38+
use Html\Element\Block\ObjectElement;
39+
use Html\Element\Block\OrderedList;
40+
use Html\Element\Block\Paragraph;
41+
use Html\Element\Block\Picture;
42+
use Html\Element\Block\PreformattedText;
43+
use Html\Element\Block\Section;
44+
use Html\Element\Block\Table;
45+
use Html\Element\Block\UnorderedList;
46+
use Html\Element\Block\Video;
1747
use Html\Element\Inline\Abbreviation;
1848
use Html\Element\Inline\Anchor;
1949
use Html\Element\Inline\BidirectionalIsolation;
@@ -45,9 +75,8 @@
4575
use Html\Element\Void\WordBreakOpportunity;
4676
use Html\Enum\AriaBusyEnum;
4777
use Html\Enum\RoleEnum;
48-
use Html\Mapping\Element;
4978
use Html\Trait\GlobalAttribute;
50-
use InvalidArgumentException;
79+
use Html\Mapping\Element;
5180

5281
#[Element('article')]
5382
class Article extends BlockElement
@@ -70,7 +99,6 @@ class Article extends BlockElement
7099
use GlobalAttribute\TitleTrait;
71100
use GlobalAttribute\TranslateTrait;
72101
use GlobalAttribute\PopoverTrait;
73-
74102
/**
75103
* The HTML element name
76104
*/
@@ -96,7 +124,7 @@ class Article extends BlockElement
96124
* @var array<string>
97125
*/
98126
public static array $childOf = [
99-
self::class,
127+
Article::class,
100128
Aside::class,
101129
Blockquote::class,
102130
Body::class,
@@ -115,7 +143,7 @@ class Article extends BlockElement
115143
Anchor::class,
116144
Abbreviation::class,
117145
Area::class,
118-
self::class,
146+
Article::class,
119147
Audio::class,
120148
Bold::class,
121149
BidirectionalIsolation::class,
@@ -168,36 +196,31 @@ class Article extends BlockElement
168196
WordBreakOpportunity::class,
169197
];
170198

171-
/**
172-
* Defines the semantic purpose of an element for assistive technologies.
173-
*/
199+
200+
/** Defines the semantic purpose of an element for assistive technologies. */
174201
public ?RoleEnum $role = null;
175202

176-
/**
177-
* Identifies the element(s) whose contents or presence are controlled by this element. Value is a list of IDs separated by a space
178-
*/
203+
/** Identifies the element(s) whose contents or presence are controlled by this element. Value is a list of IDs separated by a space */
179204
public ?string $ariaControls = null;
180205

181-
/**
182-
* Identifies the element(s) that describes the object. Value is a list of IDs separated by a space
183-
*/
206+
/** Identifies the element(s) that describes the object. Value is a list of IDs separated by a space */
184207
public ?string $ariaDescribedby = null;
185208

186-
/**
187-
* Identifies the element(s) that labels the current element. Value is a list of IDs separated by a space
188-
*/
209+
/** Identifies the element(s) that labels the current element. Value is a list of IDs separated by a space */
189210
public ?string $ariaLabelledby = null;
190211

191-
/**
212+
/**
192213
* The aria-busy attribute is used to indicate whether an element is currently busy or not.
214+
* @category HTML attribute
193215
* @example false
194216
*/
195217
public ?AriaBusyEnum $ariaBusy = null;
196218

219+
197220
public function setRole(string|RoleEnum $role): static
198221
{
199222
if (is_string($role)) {
200-
$role = RoleEnum::tryFrom($role) ?? throw new InvalidArgumentException('Invalid value for $role.');
223+
$role = RoleEnum::tryFrom($role) ?? throw new \InvalidArgumentException("Invalid value for \$role.");
201224
}
202225
$this->role = $role;
203226
$this->delegated->setAttribute('role', (string) $role->value);
@@ -249,9 +272,7 @@ public function getAriaLabelledby(): ?string
249272
public function setAriaBusy(string|AriaBusyEnum $ariaBusy): static
250273
{
251274
if (is_string($ariaBusy)) {
252-
$ariaBusy = AriaBusyEnum::tryFrom($ariaBusy) ?? throw new InvalidArgumentException(
253-
'Invalid value for $ariaBusy.'
254-
);
275+
$ariaBusy = AriaBusyEnum::tryFrom($ariaBusy) ?? throw new \InvalidArgumentException("Invalid value for \$ariaBusy.");
255276
}
256277
$this->ariaBusy = $ariaBusy;
257278
$this->delegated->setAttribute('aria-busy', (string) $ariaBusy->value);
@@ -263,4 +284,6 @@ public function getAriaBusy(): ?AriaBusyEnum
263284
{
264285
return $this->ariaBusy;
265286
}
287+
288+
266289
}

0 commit comments

Comments
 (0)