├── .env.example
├── .gitignore
├── README.md
├── TODO.md
├── bin
├── alfresco
└── help.txt
├── bootstrap.php
├── build
├── cache
│ └── .gitignore
├── indexes
│ └── .gitignore
└── output
│ └── .gitignore
├── composer.json
├── meilisearch.json
├── package.json
├── phpstan-baseline.neon
├── phpstan.neon
├── pint.json
├── postcss.config.js
├── resources
├── components
│ ├── authors.php
│ ├── caution.php
│ ├── editors.php
│ ├── emphasised-literal.php
│ ├── function-popup.php
│ ├── generic.heading-code.php
│ ├── icon.external-link.php
│ ├── icon.link.php
│ ├── inline-code.php
│ ├── link.php
│ ├── main.php
│ ├── menu-list.php
│ ├── menu.php
│ ├── note.php
│ ├── note.title.php
│ ├── ordered-list.php
│ ├── paragraph.php
│ ├── program-listing.php
│ ├── screen.php
│ ├── tip.php
│ ├── tip.title.php
│ ├── title.php
│ ├── unordered-list.php
│ └── warning.php
├── footer.html
├── header.html
├── replacements
│ └── .gitignore
├── script.js
├── style.css
└── translations
│ └── en
│ └── ui.php
├── src
├── Contracts
│ ├── DependsOnIndexes.php
│ ├── Generator.php
│ └── Slotable.php
├── Manual
│ ├── Factory.php
│ ├── Manual.php
│ └── Node.php
├── Output.php
├── Process.php
├── Render
│ ├── Factory.php
│ ├── Highlighter.php
│ ├── HtmlString.php
│ ├── HtmlTag.php
│ ├── Replacer.php
│ └── Wrapper.php
├── Stream
│ ├── FileStreamFactory.php
│ ├── State.php
│ └── Stream.php
├── Support
│ ├── Link.php
│ └── Translator.php
└── Website
│ ├── EmptyChunkIndex.php
│ ├── FunctionIndex.php
│ ├── Generator.php
│ ├── Image.php
│ ├── ImageIndex.php
│ ├── Method.php
│ ├── Parameter.php
│ ├── Title.php
│ └── TitleIndex.php
├── tailwind.config.js
├── theme.json
└── vite.config.js
/.env.example:
--------------------------------------------------------------------------------
1 | VITE_MEILISEARCH_HOST_URL="http://127.0.0.1:7700"
2 | VITE_MEILISEARCH_API_KEY="LARAVEL-HERD"
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.env
2 | /composer.lock
3 | /vendor
4 | /package-lock.json
5 | /node_modules
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🍃 Alfresco
2 |
3 | A fresh take on the PHP documentation website generation.
4 |
5 | > [!IMPORTANT]
6 | > This project is a work in progress. Follow along with thr project in this [Twitter / X](https://x.com/timacdonald87/status/1631504755225919489) thread.
7 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
3 | - [ ] Ensure titles are the correct level, i.e., h1, h2, etc.
4 | - [ ] Render a `x` for all variables with IDE-like pop up. [POC](https://twitter.com/timacdonald87/status/1647049647729553408).
5 | - [ ] Render a `f` for all functions with IDE-like pop up. [POC](https://twitter.com/timacdonald87/status/1647049647729553408).
6 | - [ ] Render a `C` for all constants with IDE-like pop up. [POC](https://twitter.com/timacdonald87/status/1647049647729553408).
7 | - [ ] Abbreviations and Acronyms should have a title where they aren't described within the sentence.
8 | - [ ] Ensure examples using `figure` scales.
9 | - [ ] The "PHP Manual" navigation item, the "PHP Manual" title on the preface page, and the "PHP Manual" HTML page need to be manually omitted.
10 | - [x] The `code` tags are forcing the font size when used in other elements, such as headings. Need a better way to make code blocks small while allowing parents to override.
11 | - [ ] Pull "see also" up into sidebar
12 | - [ ] The homepage should contain the menu in full completely expanded.
13 | - [ ] Empty pages should include a redirect to their overview page.
14 | - [ ] Links should retain their color even when they contain a `code` snippet. Currently the inline code overrides the link color.
15 | - [ ] We should have a dedicated `class` property to accompany `attributes`
16 | - [ ] Function index not handling parameter union types correctly.
17 | - [ ] function index needs to handle tags within method descriptions, like we do in titles.
18 | - [ ] I think a review of the `programlisting` and `screen` tags to ensure they are a) the correct tag to be used and b) have a role where applicable.
19 | - [ ] Images, function popups, and titles need cleanup and tightening
20 | - [ ] show line numbers when showing code snippet blocks
21 |
--------------------------------------------------------------------------------
/bin/alfresco:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | skip(1)
35 | ->filter(fn ($value) => str_starts_with($value, '-'))
36 | ->map(fn ($option) => with(explode('=', $option), fn ($bits) => [
37 | 'name' => $bits[0],
38 | 'value' => $bits[1] ?? true,
39 | ]));
40 |
41 | // Input helpers...
42 |
43 | $option = fn ($key): null|string|bool => $options->firstWhere('name', $key)['value'] ?? null;
44 | $options = fn ($key): Collection => $options->where('name', $key)->pluck('value');
45 |
46 | /*
47 | * Adjust config based on CLI input.
48 | */
49 |
50 | $option('--debug') && $container->make(Configuration::class)->set(['debug' => true]);
51 |
52 | /*
53 | * Output help information when requested.
54 | */
55 |
56 | $output = $container->make(Output::class);
57 |
58 | if ($option('--help')) {
59 | $output->write(file_get_contents(__DIR__.'/help.txt'));
60 | exit;
61 | }
62 |
63 | /*
64 | * Ensure required arguments are provided.
65 | */
66 |
67 | if (! $option('--manual')) {
68 | $container->make(Output::class)->write('The [--manual] argument is required.');
69 | exit(1);
70 | }
71 |
72 | /*
73 | * Here we go.
74 | */
75 |
76 | $output->line('🍃 Alfresco');
77 |
78 | /*
79 | * Resolve generators and their indexes.
80 | */
81 |
82 | [$generators, $indexes] = $options('--generator')
83 | ->whenEmpty(fn () => collect([Alfresco\Website\Generator::class]))
84 | ->map(fn ($class) => $container->make($class))
85 | ->pipe(fn ($generators) => [
86 | $generators,
87 | $generators->whereInstanceOf(DependsOnIndexes::class)
88 | ->flatMap(fn ($generator) => $generator->indexes()),
89 | ]);
90 |
91 | /*
92 | * Resolve the manual factory and process instances we need to run the generators.
93 | */
94 |
95 | [$factory, $process, $measure] = [
96 | $container->make(Factory::class),
97 | $container->make(Process::class),
98 | fn ($callback) => round(Benchmark::measure($callback) / 1000, 2), // @phpstan-ignore binaryOp.invalid
99 | ];
100 |
101 | /*
102 | * Run indexes.
103 | *
104 | * The indexes perform an initial loop over the documentation to perform any
105 | * useful read-ahead actions. This information can then be used by their
106 | * generators to make decisons based on data it has not yet seen.
107 | */
108 |
109 | if ($option('--no-index')) {
110 | $output->line('Skipping indexing.');
111 | } else {
112 | $output->write('Indexing');
113 |
114 | $duration = $measure(fn () => $process->handle($factory->make($option('--manual')), $indexes, function ($node, $i) use ($option, $output) {
115 | if ($option('--step-debug')) {
116 | $output->write('Hit ENTER to process node: ');
117 | $output->write("<{$node->name}");
118 | $output->write(' id="'.($node->hasId() ? $node->id() : null).'"');
119 | $output->write(' depth="'.$node->depth.'"');
120 | $output->write('>');
121 | fgets(STDIN);
122 | } elseif ($i % 5000 === 0) {
123 | $output->write('.');
124 | }
125 | }));
126 |
127 | $output->line("Indexing completed in {$duration} seconds.");
128 | }
129 |
130 | /*
131 | * Run the generators.
132 | */
133 |
134 | if ($option('--no-generation')) {
135 | $output->line('Skipping generation.');
136 | } else {
137 | $output->write('Generating');
138 |
139 | $duration = $measure(fn () => $process->handle($factory->make($option('--manual')), $generators, function ($node, $i) use ($option, $output) {
140 | if ($option('--step-debug')) {
141 | $output->write('Hit ENTER to process node: ');
142 | $output->write("<{$node->name}");
143 | $output->write(' id="'.($node->hasId() ? $node->id() : null).'"');
144 | $output->write(' depth="'.$node->depth.'"');
145 | $output->write('>');
146 | fgets(STDIN);
147 | } elseif ($i % 5000 === 0) {
148 | $output->write('.');
149 | }
150 | }));
151 |
152 | $output->line("Generation completed in {$duration} seconds.");
153 | }
154 |
155 | $output->line('Done.');
156 |
--------------------------------------------------------------------------------
/bin/help.txt:
--------------------------------------------------------------------------------
1 |
2 | 🍃 Alfresco
3 | A fresh take on the PHP documentation website generation.
4 |
5 | USAGE
6 |
7 | alfresco [--options] [generators...]
8 |
9 | OPTIONS
10 |
11 | --manual
12 | Path to the already generated manual file.
13 | Usage: alfresco --manual=../doc-base/manual.xml
14 |
15 | --no-generation
16 | Do not run the generator. Can be useful to re-generate only the indexes.
17 | Usage: alfresco --no-generation
18 |
19 | --no-index
20 | Do not run the generator's indexes. Can be useful if wanting to re-generate but the indexes are not stale.
21 | Usage: alfresco --no-index
22 |
23 | --debug
24 | Turn on debug generator output.
25 | Usage: alfresco --debug
26 |
27 | --verbose
28 | Turn on verbose build output.
29 | Usage: alfresco --debug
30 |
--------------------------------------------------------------------------------
/bootstrap.php:
--------------------------------------------------------------------------------
1 | instance(Container::class, $container);
15 |
16 | $container->singleton(Configuration::class, fn () => new Configuration([
17 | 'debug' => false,
18 | 'language' => 'en',
19 | 'root_directory' => __DIR__,
20 | 'build_directory' => __DIR__.'/build/output',
21 | 'cache_directory' => __DIR__.'/build/cache',
22 | 'resource_directory' => __DIR__.'/resources',
23 | 'index_directory' => __DIR__.'/build/indexes',
24 | 'component_directory' => __DIR__.'/resources/components',
25 | 'translation_directory' => __DIR__.'/resources/translations',
26 | 'replacements_directory' => __DIR__.'/resources/replacements',
27 | ]));
28 |
29 | $container->singleton(Factory::class);
30 |
31 | $container->bind(Shiki::class, fn (Container $container) => new Shiki(
32 | $container->make(Configuration::class)->get('root_directory').'/theme.json',
33 | ));
34 |
35 | $container->bind(FileLoader::class, fn (Container $container) => new FileLoader(
36 | $container->make(Filesystem::class),
37 | $container->make(Configuration::class)->get('translation_directory'),
38 | ));
39 |
40 | $container->singleton(Translator::class, fn (Container $container) => (new Translator(
41 | $container->make(FileLoader::class),
42 | 'en',
43 | ))->handleMissingKeysUsing(function ($key, $replace, $locale, $fallback) {
44 | throw new RuntimeException("Missing translation [{$key}] for locale [{$locale}].");
45 | }));
46 | });
47 |
--------------------------------------------------------------------------------
/build/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/build/indexes/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/build/output/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "timacdonald/alfresco",
3 | "license": "MIT",
4 | "authors": [
5 | {
6 | "name": "Tim MacDonald",
7 | "email": "hello@timacdonald.me"
8 | }
9 | ],
10 | "require": {
11 | "php": "8.3.*",
12 | "illuminate/config": "^11",
13 | "illuminate/container": "^11",
14 | "illuminate/filesystem": "^11",
15 | "illuminate/support": "^11",
16 | "illuminate/translation": "^11",
17 | "laravel/pint": "^1.18",
18 | "laravel/serializable-closure": "^1.3",
19 | "spatie/shiki-php": "^1.3",
20 | "symfony/process": "^7.1",
21 | "thecodingmachine/safe": "^2.5"
22 | },
23 | "require-dev": {
24 | "phpstan/phpstan": "^1.10",
25 | "symfony/var-dumper": "^6.2",
26 | "thecodingmachine/phpstan-safe-rule": "^1.2"
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "Alfresco\\": "src"
31 | }
32 | },
33 | "config": {
34 | "optimize-autoloader": true,
35 | "preferred-install": "dist",
36 | "sort-packages": true
37 | },
38 | "minimum-stability": "stable",
39 | "prefer-stable": true
40 | }
41 |
--------------------------------------------------------------------------------
/meilisearch.json:
--------------------------------------------------------------------------------
1 | {
2 | "index_uid": "docs",
3 | "start_urls": ["http://127.0.0.1:8080/"],
4 | "sitemap_urls": [],
5 | "stop_urls": [],
6 | "allowed_domains": ["127.0.0.1"],
7 | "selectors": {
8 | "lvl0": {
9 | "selector": "main + nav > ul > details[open] summary",
10 | "global": true,
11 | "default_value": "Documentation"
12 | },
13 | "lvl1": {
14 | "selector": "main + nav > ul > details[open] details[open] summary",
15 | "global": true,
16 | "default_value": "Sub-chapter"
17 | },
18 | "lvl2": "main h1",
19 | "lvl3": "main h2",
20 | "lvl4": "main h3",
21 | "lvl5": "main h4",
22 | "lvl6": "main h5",
23 | "text": "main"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "scripts": {
4 | "build": "vite build",
5 | "watch": "vite build --watch"
6 | },
7 | "dependencies": {
8 | "@floating-ui/dom": "^1.6.11",
9 | "autoprefixer": "^10.4.13",
10 | "meilisearch-docsearch": "^0.6.0",
11 | "postcss": "^8.4.31",
12 | "shiki": "^0.14.3",
13 | "tailwindcss": "^3.3.1",
14 | "vite": "^5.2.13"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/phpstan-baseline.neon:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timacdonald/alfresco/73e5e496e9de2f313482a6fe35f077fd6c62aec3/phpstan-baseline.neon
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 8
3 | paths:
4 | - bin/alfresco
5 | - resources/components
6 | - src
7 | includes:
8 | - ./phpstan-baseline.neon
9 | - ./vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon
10 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "blank_line_between_import_groups": false,
4 | "declare_strict_types": true,
5 | "ordered_imports": {"sort_algorithm": "length"},
6 | "strict_param": true,
7 | "trailing_comma_in_multiline": {
8 | "after_heredoc": true,
9 | "elements": ["arguments", "array_destructuring", "arrays", "match", "parameters"]
10 | },
11 | "explicit_string_variable": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/resources/components/authors.php:
--------------------------------------------------------------------------------
1 | $render->wrapper(
14 | before: $render->tag(
15 | as: 'p',
16 | class: 'my-6 first:mt-0 last:mb-0',
17 | before: $translator->get('ui.authors.by').':',
18 | ),
19 | slot: $render->component('unordered-list'),
20 | after: $render->tag(
21 | as: 'div',
22 | class: 'mb-6',
23 | ),
24 | );
25 |
--------------------------------------------------------------------------------
/resources/components/caution.php:
--------------------------------------------------------------------------------
1 | $render->tag(
14 | as: 'div',
15 | class: 'bg-orange-50/50 my-6 first:mt-0 last:mb-0 p-6 rounded border border-orange-100 text-orange-950 relative',
16 | attributes: [
17 | 'aria-role' => 'note',
18 | ],
19 | before: $render->tag(
20 | as: 'strong',
21 | class: 'absolute inline-block right-3 -top-3 rounded bg-orange-200 leading-none py-1 px-2 text-sm font-semibold font-mono uppercase',
22 | before: $translator->get('ui.caution.badge'),
23 | ),
24 | // We wrap this in a `div` to ensure that the first / last element
25 | // margin changes are not impacted by the "caution" badge we
26 | // append.
27 | slot: $render->tag('div'),
28 | );
29 |
--------------------------------------------------------------------------------
/resources/components/editors.php:
--------------------------------------------------------------------------------
1 | $render->wrapper(
14 | before: $render->inlineText($translator->get('ui.editors.by')),
15 | after: '.',
16 | );
17 |
--------------------------------------------------------------------------------
/resources/components/emphasised-literal.php:
--------------------------------------------------------------------------------
1 | $render->tag(
12 | as: 'em',
13 | class: 'not-italic',
14 | slot: $render->component('inline-code'),
15 | );
16 |
--------------------------------------------------------------------------------
/resources/components/function-popup.php:
--------------------------------------------------------------------------------
1 | $render->html(<<<'HTML'
13 |
14 | {$parameter->types->implode('|')}
15 |
16 |
17 | \${$parameter->name}
18 |
19 | HTML);
20 |
21 | return function (
22 | string $id,
23 | Method $method,
24 | Factory $render,
25 | ) use ($parameter) {
26 | $description = Str::of($method->description)->trim()->finish('.');
27 |
28 | return $render->html(<<
30 |
/**
31 | * {$description}
32 | */
33 |
34 | {$method->name}
35 |
36 | ({$method->parameters->map(fn (Parameter $p) => $render->component($parameter, ['parameter' => $p]))->implode(', ')}):
37 |
38 | {$method->returnTypes->implode('|')}
39 |
40 |
41 | HTML
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/resources/components/generic.heading-code.php:
--------------------------------------------------------------------------------
1 | $render->tag(
12 | as: 'code',
13 | class: 'font-mono',
14 | );
15 |
--------------------------------------------------------------------------------
/resources/components/icon.external-link.php:
--------------------------------------------------------------------------------
1 | <<
13 | {$translator->get('ui.link.external')}
14 |
15 |
16 | HTML;
17 |
--------------------------------------------------------------------------------
/resources/components/icon.link.php:
--------------------------------------------------------------------------------
1 | <<<'HTML'
6 |
10 | HTML;
11 |
--------------------------------------------------------------------------------
/resources/components/inline-code.php:
--------------------------------------------------------------------------------
1 | $render->tag(
15 | as: $as,
16 | class: [
17 | 'text-[0.875em] not-italic bg-violet-950/5 text-violet-950 rounded leading-none py-1 px-1.5 inline-block font-mono',
18 | ...(is_array($class) ? $class : [$class]),
19 | ],
20 | attributes: $attributes,
21 | );
22 |
--------------------------------------------------------------------------------
/resources/components/link.php:
--------------------------------------------------------------------------------
1 | $render->tag(
15 | as: 'a',
16 | class: [
17 | 'text-violet-700 hover:underline group',
18 | $link->isInternal ? '' : 'inline-flex items-center',
19 | ],
20 | attributes: [
21 | 'href' => $link->isInternal
22 | ? "{$link->destination}.html"
23 | : $link->destination,
24 | 'rel' => $link->isInternal
25 | ? 'prefetch'
26 | : false,
27 | ],
28 | before: $text,
29 | after: $link->isInternal ? '' : $render->component('icon.external-link'),
30 | );
31 |
--------------------------------------------------------------------------------
/resources/components/main.php:
--------------------------------------------------------------------------------
1 | $render->tag(
12 | as: 'main',
13 | class: 'max-w-4xl w-full px-12 pb-8 pt-[23px] text-slate-800 leading-relaxed text-base font-normal',
14 | );
15 |
--------------------------------------------------------------------------------
/resources/components/menu-list.php:
--------------------------------------------------------------------------------
1 | $items->map(fn (Title $title) => $title->children->isEmpty()
17 | ? ($empty->contains(fn (Title $empty) => $title->is($empty)) ? '' : $render->tag(
18 | as: 'div',
19 | class: [
20 | 'relative flex items-center pl-4 py-1.5 before:block before:h-1.5 before:w-1.5 before:rounded-full before:absolute before:left-[3px]',
21 | $title->is($active) ? 'before:bg-violet-600' : 'before:bg-violet-300',
22 | ],
23 | before: $render->tag(
24 | as: 'a',
25 | class: [
26 | 'text-sm relative flex items-center',
27 | $title->is($active) ? 'text-violet-700 font-bold' : 'text-violet-950',
28 | ],
29 | attributes: [
30 | 'href' => "{$title->id}.html",
31 | ],
32 | before: $title->html,
33 | ),
34 | )->toString())
35 | : $render->tag(
36 | as: 'details',
37 | class: 'pl-4',
38 | attributes: [
39 | 'open' => $title->isOrHasChild($active),
40 | ],
41 | before: $render->tag(
42 | as: 'summary',
43 | class: 'cursor-pointer py-1.5 list-outside text-violet-950 text-sm marker:text-violet-300',
44 | before: $title->html,
45 | ),
46 | after: ($empty->contains(fn (Title $empty) => $title->is($empty))
47 | ? ''
48 | : $render->tag(
49 | as: 'div',
50 | class: [
51 | 'relative flex items-center pl-4 py-1.5 before:block before:h-1.5 before:w-1.5 before:rounded-full before:absolute before:left-[3px]',
52 | $title->is($active) ? 'before:bg-violet-600' : 'before:bg-violet-300',
53 | ],
54 | before: $render->tag(
55 | as: 'a',
56 | class: [
57 | 'text-sm relative flex items-center',
58 | $title->is($active) ? 'text-violet-700 font-bold' : 'text-violet-950',
59 | ],
60 | attributes: [
61 | 'href' => "{$title->id}.html",
62 | ],
63 | before: $title->html.' overview',
64 | ),
65 | )).$render->component('menu-list', [
66 | 'items' => $title->children,
67 | 'active' => $active,
68 | 'empty' => $empty,
69 | ]),
70 | )->toString())->join('');
71 |
--------------------------------------------------------------------------------
/resources/components/menu.php:
--------------------------------------------------------------------------------
1 | $render->tag(
17 | as: 'nav',
18 | class: 'order-first py-4 pl-10 pr-6 w-[300px] bg-violet-25 border-r border-violet-100',
19 | attributes: [
20 | 'id' => 'main-nav',
21 | ],
22 | before: $render->tag(
23 | as: 'div',
24 | class: 'list-none my-6 first:mt-0 last:mb-0 -ml-4',
25 | before: $render->component('menu-list', [
26 | 'items' => $items,
27 | 'active' => $active,
28 | 'empty' => $empty,
29 | ]),
30 | ),
31 | );
32 |
--------------------------------------------------------------------------------
/resources/components/note.php:
--------------------------------------------------------------------------------
1 | $render->tag(
14 | as: 'div',
15 | class: 'bg-blue-50/50 my-6 first:mt-0 last:mb-0 p-6 rounded border border-blue-100 text-blue-950 relative',
16 | attributes: [
17 | 'aria-role' => 'note',
18 | ],
19 | before: $render->tag(
20 | as: 'strong',
21 | class: 'absolute inline-block right-3 -top-3 rounded bg-blue-200 leading-none py-1 px-2 text-sm font-semibold font-mono uppercase',
22 | before: $translator->get('ui.note.badge'),
23 | ),
24 | // We wrap this in a `div` to ensure that the first / last element
25 | // margin changes are not impacted by the "note" badge we
26 | // append.
27 | slot: $render->tag('div'),
28 | );
29 |
--------------------------------------------------------------------------------
/resources/components/note.title.php:
--------------------------------------------------------------------------------
1 | $render->tag(
14 | as: 'h3',
15 | class: 'text-xl text-blue-950 items-center text-lg font-bold leading-none inline-block',
16 | attributes: [
17 | 'id' => $link->destinationWithoutFragmentHash(),
18 | ],
19 | slot: $render->tag(
20 | as: 'a',
21 | class: 'inline-flex items-center relative group hover:underline',
22 | attributes: [
23 | 'href' => $link->destination,
24 | ],
25 | after: $render->tag(
26 | as: 'span',
27 | class: 'hidden items-center justify-center group-hover:flex leading-none absolute w-5 h-full -right-7 top-0',
28 | attributes: [
29 | 'aria-hidden' => 'true',
30 | ],
31 | after: $render->component('icon.link'),
32 | ),
33 | ),
34 | );
35 |
--------------------------------------------------------------------------------
/resources/components/ordered-list.php:
--------------------------------------------------------------------------------
1 | $render->tag(
13 | as: 'ol',
14 | class: 'list-decimal pl-5 my-2 last:mb-0 space-y-2',
15 | attributes: [
16 | 'type' => $type,
17 | ],
18 | );
19 |
--------------------------------------------------------------------------------
/resources/components/paragraph.php:
--------------------------------------------------------------------------------
1 | $render->tag(
12 | as: 'p',
13 | class: 'my-6 first:mt-0 last:mb-0',
14 | );
15 |
--------------------------------------------------------------------------------
/resources/components/program-listing.php:
--------------------------------------------------------------------------------
1 | $render->tag(
12 | as: 'div',
13 | class: 'border border-violet-100 bg-violet-25 px-6 py-5 leading-7 rounded my-6 first:mt-0 last:mb-0 relative',
14 | before: <<<'SVG'
15 |
18 | SVG,
19 | slot: $render->tag(
20 | as: 'pre',
21 | class: 'overflow-x-auto',
22 | slot: $render->tag(
23 | as: 'code',
24 | class: 'font-mono',
25 | ),
26 | ),
27 | );
28 |
--------------------------------------------------------------------------------
/resources/components/screen.php:
--------------------------------------------------------------------------------
1 | $render->tag(
12 | as: 'div',
13 | class: 'border border-violet-100 bg-violet-25 px-6 py-5 leading-7 rounded my-6 first:mt-0 last:mb-0 relative ',
14 | slot: $render->tag(
15 | as: 'pre',
16 | class: 'overflow-x-auto',
17 | slot: $render->tag(
18 | as: 'code',
19 | class: 'font-mono text-slate-950',
20 | ),
21 | ),
22 | );
23 |
--------------------------------------------------------------------------------
/resources/components/tip.php:
--------------------------------------------------------------------------------
1 | $render->tag(
14 | as: 'div',
15 | class: 'bg-green-50/50 my-6 first:mt-0 last:mb-0 p-6 rounded border border-green-200 text-green-950 relative',
16 | attributes: [
17 | 'aria-role' => 'note',
18 | ],
19 | before: $render->tag(
20 | as: 'strong',
21 | class: 'absolute inline-block right-3 -top-3 rounded bg-green-200 leading-none py-1 px-2 text-sm font-semibold font-mono uppercase',
22 | before: $translator->get('ui.tip.badge'),
23 | ),
24 | // We wrap this in a `div` to ensure that the first / last element
25 | // margin changes are not impacted by the "note" badge we
26 | // append.
27 | slot: $render->tag('div'),
28 | );
29 |
--------------------------------------------------------------------------------
/resources/components/tip.title.php:
--------------------------------------------------------------------------------
1 | $render->tag(
14 | as: 'h3',
15 | class: 'text-xl text-blue-950 items-center text-lg font-bold leading-none inline-block',
16 | attributes: [
17 | 'id' => $link->destinationWithoutFragmentHash(),
18 | ],
19 | slot: $render->tag(
20 | as: 'a',
21 | class: 'inline-flex items-center relative group hover:underline',
22 | attributes: [
23 | 'href' => $link->destination,
24 | ],
25 | after: $render->tag(
26 | as: 'span',
27 | class: 'hidden items-center justify-center group-hover:flex leading-none absolute w-5 h-full -right-7 top-0',
28 | attributes: [
29 | 'aria-hidden' => 'true',
30 | ],
31 | after: $render->component('icon.link'),
32 | ),
33 | ),
34 | );
35 |
--------------------------------------------------------------------------------
/resources/components/title.php:
--------------------------------------------------------------------------------
1 | $render->tag(
15 | as: match ($level) {
16 | 1 => 'h1',
17 | 2 => 'h2',
18 | 6 => 'h6',
19 | },
20 | class: [
21 | 'font-extrabold text-violet-950 leading-tight tracking-tight',
22 | match ($level) {
23 | // Due to line-height the first heading on a page doesn't have
24 | // consistent spacing on top and sides. We will just "pull" it
25 | // up a few pixels.
26 | 1 => 'text-4xl -mt-3',
27 | 2 => 'text-2xl my-6',
28 | 6 => 'bg-red-500',
29 | },
30 | ],
31 | attributes: [
32 | 'id' => $link->destinationWithoutFragmentHash(),
33 | ],
34 | slot: $render->tag(
35 | as: 'a',
36 | class: 'relative group hover:underline',
37 | attributes: [
38 | 'href' => $link->destination,
39 | ],
40 | after: $render->tag(
41 | as: 'span',
42 | class: 'hidden items-center group-hover:flex leading-none absolute w-5 h-full -right-7 top-0',
43 | attributes: [
44 | 'aria-hidden' => 'true',
45 | ],
46 | after: $render->component('icon.link'),
47 | ),
48 | ),
49 | );
50 |
--------------------------------------------------------------------------------
/resources/components/unordered-list.php:
--------------------------------------------------------------------------------
1 | $render->tag(
12 | as: 'ul',
13 | class: 'list-disc pl-5 my-3 space-y-2',
14 | );
15 |
--------------------------------------------------------------------------------
/resources/components/warning.php:
--------------------------------------------------------------------------------
1 | $render->tag(
14 | as: 'div',
15 | class: 'bg-red-50/50 my-6 first:mt-0 last:mb-0 p-6 rounded border border-red-100 text-red-950 relative',
16 | attributes: [
17 | 'aria-role' => 'note',
18 | ],
19 | before: $render->tag(
20 | as: 'strong',
21 | class: 'absolute inline-block right-3 -top-3 rounded bg-red-200 leading-none py-1 px-2 text-sm font-semibold font-mono uppercase',
22 | before: $translator->get('ui.warning.badge'),
23 | ),
24 | // We wrap this in a `div` to ensure that the first / last element
25 | // margin changes are not impacted by the "caution" badge we
26 | // append.
27 | slot: $render->tag('div'),
28 | );
29 |
--------------------------------------------------------------------------------
/resources/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | <?
15 |
18 | ?>
19 |
20 |
21 |
22 |
23 | Downloads
24 |
25 |
26 | Get Involved
27 |
28 |
29 | Help
30 |
31 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/resources/replacements/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/resources/script.js:
--------------------------------------------------------------------------------
1 | import { docsearch } from "meilisearch-docsearch";
2 | import "meilisearch-docsearch/css";
3 | import { computePosition, flip, offset, shift } from "@floating-ui/dom";
4 |
5 | docsearch({
6 | container: "#docsearch",
7 | host: import.meta.env.VITE_MEILISEARCH_HOST_URL,
8 | apiKey: import.meta.env.VITE_MEILISEARCH_API_KEY,
9 | indexUid: "docs",
10 | hotKeys: '/'
11 | })
12 |
13 | const updateTooltip = (tooltip, button) => computePosition(button, tooltip, {
14 | placement: 'top',
15 | middleware: [
16 | flip(),
17 | shift({ padding: 6 }),
18 | offset(6)
19 | ],
20 | }).then(({x, y}) => {
21 | Object.assign(tooltip.style, {
22 | left: `${x}px`,
23 | top: `${y}px`,
24 | });
25 | });
26 |
27 | const showTooltip = (tooltip, button) => {
28 | tooltip.style['transition-delay'] = '200ms'
29 | tooltip.style.display = 'block'
30 | updateTooltip(tooltip, button);
31 | tooltip.classList.add('active')
32 | hoverOutTimeouts
33 | .filter((v) => v.button === button)
34 | .forEach((hoverOutTimeout) => window.clearTimeout(hoverOutTimeout.timeoutId))
35 |
36 | hoverOutTimeouts.push({ button, timeoutId: setTimeout(() => tooltip.style['transition-delay'] = '0ms', 200)})
37 | }
38 |
39 | const hideTooltip = (tooltip, button) => {
40 | tooltip.classList.remove('active')
41 |
42 | hoverOutTimeouts.find((v) => v.button === button).timeoutId = window.setTimeout(() => {
43 | tooltip.style.display = 'none'
44 | tooltip.style['transition-delay'] = '0ms'
45 | }, 200)
46 | }
47 |
48 |
49 | const buttons = document.querySelectorAll('button[tooltip-target]')
50 | const hoverOutTimeouts = [];
51 | buttons.forEach((button) => hoverOutTimeouts.push({ button, timeoutId: null }));
52 |
53 | [
54 | ['mouseenter', showTooltip],
55 | ['mouseleave', hideTooltip],
56 | ['focus', showTooltip],
57 | ['blur', hideTooltip],
58 | ].forEach(([event, listener]) => {
59 | buttons.forEach((button) => {
60 | const tooltip = document.getElementById(button.getAttribute('tooltip-target'))
61 |
62 | button.addEventListener(event, () => listener(tooltip, button));
63 | })
64 | });
65 |
66 | // const prefetched = new Set
67 |
68 | // const prefetch = href => {
69 | // if (prefetched.has(href)) {
70 | // return
71 | // }
72 |
73 | // prefetched.add(href)
74 |
75 | // const link = document.createElement('link')
76 | // link.href = href
77 | // link.rel = 'prefetch'
78 | // link.as = 'document'
79 | // link.fetchPriority = 'low'
80 |
81 | // document.head.appendChild(link)
82 | // }
83 |
84 | // const anchors = () => [
85 | // ...document.querySelectorAll('a[href][rel~="prefetch"]')
86 | // ].filter(el => {
87 | // try {
88 | // const url = new URL(el.href)
89 |
90 | // return window.location.origin === url.origin && window.location.pathname !== url.pathname
91 | // } catch {
92 | // return false
93 | // }
94 | // })
95 |
96 | // const observer = new IntersectionObserver((entries, observer) => {
97 | // entries.forEach(entry => {
98 | // if (entry.isIntersecting) {
99 | // observer.unobserve(entry.target)
100 | // prefetch(entry.target.href)
101 | // }
102 | // })
103 | // }, { threshold: 1.0 });
104 |
105 | // // requestIdleCallback(() => {
106 | // setTimeout(() => {
107 | // if (window.navigator?.connection?.saveData === true) {
108 | // return;
109 | // }
110 |
111 | // if (/(2|3)g/.test(window.navigator?.connection?.effectiveType ?? '')) {
112 | // return;
113 | // }
114 |
115 | // anchors().forEach(el => observer.observe(el))
116 | // })
117 |
--------------------------------------------------------------------------------
/resources/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | #docsearch .docsearch-btn {
6 | color: theme('colors.violet.500');
7 | height: 33px;
8 | background: theme('colors.violet.25');
9 | border: 1px solid theme('colors.violet.100');
10 | }
11 |
12 | #docsearch .docsearch-modal-btn-icon {
13 | height: 20px;
14 | width: 20px;
15 | }
16 |
17 | #docsearch .docsearch-btn-keys {
18 | align-items: center;
19 | content: '/'
20 | }
21 |
22 | #docsearch .docsearch-btn-keys::after {
23 | display: block;
24 | height: 100%;
25 | content: "/";
26 | }
27 |
28 | #docsearch .docsearch-btn-key {
29 | display: none;
30 | }
31 |
32 | div[role="tooltip"] {
33 | display: none;
34 | opacity: 0;
35 | transform: scale(0.8) translateY(-1.5rem);
36 | width: max-content;
37 | position: absolute;
38 | top: 0;
39 | left: 0;
40 | backdrop-filter: blur(8px);
41 | background: rgba(255, 255, 255, 0.8);
42 | color: theme('colors.slate.800');
43 | font-family: theme('fontFamily.mono');
44 | padding: 12px;
45 | box-shadow: theme('boxShadow.xl');
46 | border-radius: 8px;
47 | font-size: 0.875rem;
48 | transition-property: opacity, transform;
49 | transition-duration: 200ms;
50 | transition-timing-function: ease-out;
51 | }
52 |
53 | div[role="tooltip"].active {
54 | transform: scale(1) translateY(0px);
55 | opacity: 1;
56 | }
57 |
--------------------------------------------------------------------------------
/resources/translations/en/ui.php:
--------------------------------------------------------------------------------
1 | 'Documentation by',
7 | 'caution.badge' => 'Caution',
8 | 'editors.by' => 'Edited by',
9 | 'link.external' => 'External link',
10 | 'note.badge' => 'Note',
11 | 'tip.badge' => 'Tip',
12 | 'warning.badge' => 'Warning',
13 | ];
14 |
--------------------------------------------------------------------------------
/src/Contracts/DependsOnIndexes.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | public function indexes(): array;
15 | }
16 |
--------------------------------------------------------------------------------
/src/Contracts/Generator.php:
--------------------------------------------------------------------------------
1 | reader($path));
28 | }
29 |
30 | /**
31 | * Make a base XML reader for the given path.
32 | */
33 | protected function reader(string $path): XMLReader
34 | {
35 | $reader = XMLReader::open($path, 'UTF-8');
36 |
37 | if ($reader === false) {
38 | throw new RuntimeException('Unable to create XML reader.');
39 | }
40 |
41 | return $reader;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Manual/Manual.php:
--------------------------------------------------------------------------------
1 |
31 | */
32 | protected array $parents = [];
33 |
34 | /**
35 | * The current node's index.
36 | */
37 | protected int $cursorIndex = -1;
38 |
39 | /**
40 | * The previous node's depth.
41 | */
42 | protected int $lastDepth = -1;
43 |
44 | /**
45 | * Create a new instance.
46 | */
47 | public function __construct(
48 | protected XMLReader $xml,
49 | ) {
50 | //
51 | }
52 |
53 | /**
54 | * Read the next node of the manual.
55 | */
56 | public function read(): ?Node
57 | {
58 | if (! $this->xml->read()) {
59 | $this->xml->close();
60 |
61 | return null;
62 | }
63 |
64 | $previousSibling = $this->previousSibling();
65 |
66 | $this->cursorIndex = $this->cursorIndex + 1;
67 |
68 | $this->lastDepth = $this->xml->depth;
69 |
70 | return tap($this->node($this->cursorIndex, $previousSibling), $this->rememberNode(...));
71 | }
72 |
73 | /**
74 | * The current node instance.
75 | */
76 | protected function node(int $index, ?string $previousSibling): Node
77 | {
78 | return new Node(
79 | name: $this->xml->name,
80 | type: $this->xml->nodeType,
81 | value: $this->xml->value,
82 | depth: $this->xml->depth,
83 | language: $this->xml->xmlLang,
84 | namespace: $this->xml->namespaceURI,
85 | isSelfClosing: $this->xml->isEmptyElement,
86 | attributes: $this->attributes(),
87 | previousSibling: $previousSibling,
88 | parent: $this->parents[$this->xml->depth - 1] ?? null,
89 | innerContentResolver: function () use ($index) {
90 | if ($index !== $this->cursorIndex) {
91 | throw new RuntimeException('Unable to read the XML contents as the cursor has moved past the current node.');
92 | }
93 |
94 | return $this->xml->readInnerXml();
95 | },
96 | );
97 | }
98 |
99 | /**
100 | * Remember node for use as a parent.
101 | */
102 | protected function rememberNode(Node $node): void
103 | {
104 | if ($this->xml->nodeType === XMLReader::ELEMENT) {
105 | $this->parents[$this->xml->depth] = $node;
106 | }
107 | }
108 |
109 | /**
110 | * The current node's previous sibling tag name.
111 | */
112 | protected function previousSibling(): ?string
113 | {
114 | return $this->lastDepth >= $this->xml->depth
115 | ? $this->parents[$this->xml->depth]->name ?? null
116 | : null;
117 | }
118 |
119 | /**
120 | * The attributes for the current node.
121 | *
122 | * @return array
>
123 | */
124 | protected function attributes(): array
125 | {
126 | $attributes = [];
127 |
128 | if ($this->xml->hasAttributes) {
129 | $this->xml->moveToFirstAttribute();
130 |
131 | do {
132 | $attributes[$this->xml->namespaceURI ?: static::XMLNS_DOCBOOK][$this->xml->localName] = $this->xml->value;
133 | } while ($this->xml->moveToNextAttribute());
134 |
135 | $this->xml->moveToElement();
136 | }
137 |
138 | return $attributes; // @phpstan-ignore return.type
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Manual/Node.php:
--------------------------------------------------------------------------------
1 | > $attributes
18 | */
19 | public function __construct(
20 | public string $name,
21 | public int $type,
22 | public string $value,
23 | public int $depth,
24 | public string $language,
25 | public string $namespace,
26 | public bool $isSelfClosing,
27 | public array $attributes,
28 | public ?string $previousSibling,
29 | public ?Node $parent,
30 | public Closure $innerContentResolver,
31 | ) {
32 | //
33 | }
34 |
35 | /**
36 | * Retrieve node's ID attribute.
37 | */
38 | public function id(): string
39 | {
40 | if (! $this->hasId()) {
41 | throw new RuntimeException('Node is missing ID');
42 | }
43 |
44 | return $this->attributes[Manual::XMLNS_XML]['id'];
45 | }
46 |
47 | /**
48 | * Determine if the node has an ID attribute.
49 | */
50 | public function hasId(?string $id = null): bool
51 | {
52 | if ($id === null) {
53 | return isset($this->attributes[Manual::XMLNS_XML]['id']);
54 | } else {
55 | return ($this->attributes[Manual::XMLNS_XML]['id'] ?? null) === $id;
56 | }
57 | }
58 |
59 | /**
60 | * Retrieve the node's role attribute.
61 | */
62 | public function role(): string
63 | {
64 | return $this->attributes[Manual::XMLNS_DOCBOOK]['role'];
65 | }
66 |
67 | /**
68 | * Determine if the node has a role attribute.
69 | */
70 | public function hasRole(): bool
71 | {
72 | return isset($this->attributes[Manual::XMLNS_DOCBOOK]['role']);
73 | }
74 |
75 | /**
76 | * Retrieve the referenced link.
77 | */
78 | public function link(): Link
79 | {
80 | if (isset($this->attributes[Manual::XMLNS_DOCBOOK]['linkend'])) {
81 | return Link::internal($this->attributes[Manual::XMLNS_DOCBOOK]['linkend']);
82 | }
83 |
84 | return with($this->attributes[Manual::XMLNS_XLINK]['href'], function (string $href) {
85 | if (str_starts_with('https://www.php.net/', $href)) {
86 | return Link::internal($href);
87 | }
88 |
89 | return Link::external($href);
90 | });
91 | }
92 |
93 | /**
94 | * Retrieve the given attribute.
95 | */
96 | public function attribute(string $name): string
97 | {
98 | return $this->attributes[Manual::XMLNS_DOCBOOK][$name];
99 | }
100 |
101 | /**
102 | * Determine if the node is whitespace.
103 | */
104 | public function isWhitespace(): bool
105 | {
106 | return in_array($this->type, [
107 | XMLReader::WHITESPACE,
108 | XMLReader::SIGNIFICANT_WHITESPACE,
109 | ], true);
110 | }
111 |
112 | /**
113 | * Determine if the node is an opening element.
114 | */
115 | public function isOpeningElement(): bool
116 | {
117 | return $this->type === XMLReader::ELEMENT;
118 | }
119 |
120 | /**
121 | * Determine if the node is a closing element.
122 | */
123 | public function isClosingElement(): bool
124 | {
125 | return $this->type === XMLReader::END_ELEMENT;
126 | }
127 |
128 | /**
129 | * Determine if the node is text content.
130 | */
131 | public function isTextContent(): bool
132 | {
133 | return $this->type === XMLReader::TEXT;
134 | }
135 |
136 | /**
137 | * Determine if the node is CDATA.
138 | */
139 | public function isCData(): bool
140 | {
141 | return $this->type === XMLReader::CDATA;
142 | }
143 |
144 | /**
145 | * Determine if the node is a processing instruction.
146 | */
147 | public function isProcessingInstruction(): bool
148 | {
149 | return $this->type === XMLReader::PI;
150 | }
151 |
152 | /**
153 | * Determine if the node is a comment.
154 | */
155 | public function isComment(): bool
156 | {
157 | return $this->type === XMLReader::COMMENT;
158 | }
159 |
160 | /**
161 | * Determine if the node is a doctype.
162 | */
163 | public function isDoctype(): bool
164 | {
165 | return $this->type === XMLReader::DOC_TYPE;
166 | }
167 |
168 | /**
169 | * Determine if the node has the given previous sibling.
170 | */
171 | public function hasPreviousSibling(string $sibling): bool
172 | {
173 | return $this->previousSibling === $sibling;
174 | }
175 |
176 | public function expectParent(string $path): Node
177 | {
178 | $parent = $this->parent($path);
179 |
180 | if ($parent === null) {
181 | throw new RuntimeException("Expected parent was not found. From [{$this->lineage()}] looking for parent [{$path}].");
182 | }
183 |
184 | return $parent;
185 | }
186 |
187 | /**
188 | * Retrieve the given parent.
189 | *
190 | * Supports dot notation, e.g., path: "type.methodparameter"
191 | */
192 | public function parent(string $path): ?Node
193 | {
194 | $node = $this;
195 |
196 | foreach (explode('.', $path) as $name) {
197 | if ($node->parent?->name === $name) {
198 | $node = $node->parent;
199 | } else {
200 | return null;
201 | }
202 | }
203 |
204 | return $node;
205 | }
206 |
207 | /**
208 | * Determine if the node has the given parent.
209 | *
210 | * Supports dot notation, e.g., path: "type.methodparameter"
211 | */
212 | public function hasParent(?string $path = null): bool
213 | {
214 | if ($path === null) {
215 | return $this->parent !== null;
216 | }
217 |
218 | return $this->parent($path) !== null;
219 | }
220 |
221 | /**
222 | * Determine if the node has no parent.
223 | */
224 | public function hasNoParent(): bool
225 | {
226 | return $this->parent === null;
227 | }
228 |
229 | /**
230 | * Retrieve the node's lineage as a dot separated path.
231 | */
232 | public function lineage(): ?string
233 | {
234 | if ($this->parent === null) {
235 | return null;
236 | }
237 |
238 | $node = $this;
239 | $parents = '';
240 |
241 | while ($node = $node->parent) {
242 | $parents .= "{$node->name}";
243 |
244 | if ($node->hasId()) {
245 | $parents .= "[{$node->id()}]";
246 | }
247 |
248 | $parents .= '.';
249 | }
250 |
251 | return rtrim($parents, '.');
252 | }
253 |
254 | /**
255 | * Retrieve the number of ancestors the node has.
256 | */
257 | public function countAncestors(string $name): int
258 | {
259 | $node = $this;
260 | $count = 0;
261 |
262 | while ($node = $node->parent) {
263 | if ($node->name === $name) {
264 | $count++;
265 | }
266 | }
267 |
268 | return $count;
269 | }
270 |
271 | /**
272 | * Retrieve the given ancestor for the node.
273 | */
274 | public function ancestor(string $name): ?Node
275 | {
276 | $node = $this;
277 |
278 | while ($node = $node->parent) {
279 | if ($node->name === $name) {
280 | return $node;
281 | }
282 | }
283 |
284 | return null;
285 | }
286 |
287 | /**
288 | * Determine if the node has the given ancestor.
289 | */
290 | public function hasAncestor(string $name): bool
291 | {
292 | return $this->ancestor($name) !== null;
293 | }
294 |
295 | /**
296 | * Retrieve the list numeration type fo the given node.
297 | */
298 | public function numeration(): string
299 | {
300 | if ($this->name !== 'orderedlist') {
301 | throw new RuntimeException('Numeration is only accessible on orderedlist nodes.');
302 | }
303 |
304 | return match ($this->attributes[Manual::XMLNS_DOCBOOK]['numeration'] ?? null) {
305 | 'upperalpha' => 'A',
306 | 'loweralpha' => 'a',
307 | 'upperroman' => 'I',
308 | 'lowerroman' => 'i',
309 | null => '1',
310 | default => throw new RuntimeException('Unknown numeration type.'),
311 | };
312 | }
313 |
314 | /**
315 | * Determine if the node objects to chunking.
316 | */
317 | public function objectsToChunking(): bool
318 | {
319 | return isset($this->attributes[Manual::XMLNS_DOCBOOK]['annotations'])
320 | && str_contains($this->attributes[Manual::XMLNS_DOCBOOK]['annotations'], 'chunk:false');
321 | }
322 |
323 | /**
324 | * Retrieve the node's inner content.
325 | */
326 | public function innerContent(): string
327 | {
328 | return once($this->innerContentResolver);
329 | }
330 |
331 | /**
332 | * Determine if the node has the given attribute.
333 | */
334 | public function hasAttribute(string $name): bool
335 | {
336 | foreach ($this->attributes as $namespace => $attributes) {
337 | if (array_key_exists($name, $attributes)) {
338 | return true;
339 | }
340 | }
341 |
342 | return false;
343 | }
344 |
345 | /**
346 | * Determine if the node has any attributes.
347 | */
348 | public function hasAnyAttributes(): bool
349 | {
350 | return $this->attributes !== [];
351 | }
352 |
353 | /**
354 | * Determine if the node wants to chunk.
355 | */
356 | public function wantsToChunk(): bool
357 | {
358 | return in_array($this->name, [
359 | 'book',
360 | 'chapter',
361 | 'legalnotice',
362 | 'preface',
363 | 'sect1',
364 | 'section',
365 | ], true) && $this->hasId() && ! $this->objectsToChunking();
366 | }
367 | }
368 |
--------------------------------------------------------------------------------
/src/Output.php:
--------------------------------------------------------------------------------
1 | lineWritten = false;
20 |
21 | echo str($message)
22 | ->replace([
23 | '',
24 | '',
25 | '',
26 | '',
27 | '',
28 | '',
29 | '',
30 | '',
31 | '',
32 | '',
33 | ], [
34 | "\033[1m", // bold
35 | "\033[22m",
36 | "\033[92m", // green
37 | "\033[39m",
38 | "\033[93m", // yellow
39 | "\033[39m",
40 | "\033[94m", // blue
41 | "\033[39m",
42 | "\033[2m", // dim
43 | "\033[22m",
44 | ]);
45 |
46 | return $this;
47 | }
48 |
49 | /**
50 | * Write the given line.
51 | */
52 | public function line(string $message = ''): Output
53 | {
54 | if (! $this->lineWritten) {
55 | $message = PHP_EOL.$message;
56 | }
57 |
58 | $this->write($message.PHP_EOL);
59 |
60 | $this->lineWritten = true;
61 |
62 | return $this;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Process.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | protected SplObjectStorage $streams;
24 |
25 | /**
26 | * Pending "closers" for each generator.
27 | *
28 | * @var SplObjectStorage>
29 | */
30 | protected SplObjectStorage $closers;
31 |
32 | /**
33 | * Process the manual against the given generators.
34 | *
35 | * @param iterable $generators
36 | */
37 | public function handle(Manual $manual, iterable $generators, ?Closure $onTick = null): void
38 | {
39 | $this->streams = new SplObjectStorage;
40 | $this->closers = new SplObjectStorage;
41 |
42 | $onTick ??= fn () => null;
43 | $iteration = 0;
44 |
45 | foreach ($generators as $generator) {
46 | $generator->setUp();
47 | $this->closers[$generator] = [];
48 | }
49 |
50 | while ($node = $manual->read()) {
51 | if ($node->isDoctype()) {
52 | continue;
53 | }
54 |
55 | $onTick($node, $iteration++);
56 |
57 | foreach ($generators as $generator) {
58 | if ($iteration === 1) {
59 | $this->streams[$generator] = $generator->stream($node);
60 | }
61 |
62 | if ($node->isOpeningElement()) {
63 | $this->handleOpeningElement($generator, $node);
64 |
65 | continue;
66 | }
67 |
68 | if ($node->isClosingElement()) {
69 | $this->handleClosingElement($generator, $node);
70 |
71 | continue;
72 | }
73 |
74 | if (
75 | $node->isTextContent() ||
76 | $node->isCData() ||
77 | $node->isProcessingInstruction()
78 | ) {
79 | $this->write($generator, $generator->render($node), $node);
80 |
81 | continue;
82 | }
83 |
84 | if (
85 | $node->isWhitespace() ||
86 | $node->isComment()
87 | ) {
88 | continue;
89 | }
90 |
91 | throw new RuntimeException("Encountered an unhandled node of type [{$node->type}] with the name [{$node->name}].");
92 | }
93 | }
94 |
95 | foreach ($generators as $generator) {
96 | $this->writePendingClosers($generator);
97 |
98 | $generator->tearDown();
99 |
100 | $this->streams[$generator]->close();
101 | }
102 |
103 | $this->streams = new SplObjectStorage;
104 | $this->closers = new SplObjectStorage;
105 | }
106 |
107 | /**
108 | * Handle an opening element.
109 | */
110 | protected function handleOpeningElement(Generator $generator, Node $node): void
111 | {
112 | if ($generator->shouldChunk($node)) {
113 | $this->writePendingClosers($generator);
114 |
115 | $this->streams[$generator]->close();
116 |
117 | $this->streams[$generator] = $generator->stream($node);
118 | }
119 |
120 | $this->write($generator, $generator->render($node), $node);
121 | }
122 |
123 | /**
124 | * Handle an closing element.
125 | */
126 | protected function handleClosingElement(Generator $generator, Node $node): void
127 | {
128 | if ($this->matchesNextPendingCloser($generator, $node)) {
129 | $this->writeNextCloser($generator);
130 | }
131 | }
132 |
133 | /**
134 | * Determine if the node matches the next pending closer.
135 | */
136 | protected function matchesNextPendingCloser(Generator $generator, Node $node): bool
137 | {
138 | if ($this->closers[$generator] === []) {
139 | return false;
140 | }
141 |
142 | return with($this->closers[$generator][array_key_last($this->closers[$generator])][1], function (Node $openingNode) use ($node) {
143 | return $openingNode->name === $node->name && $openingNode->depth === $node->depth;
144 | });
145 | }
146 |
147 | /**
148 | * Write all pending closers.
149 | */
150 | protected function writePendingClosers(Generator $generator): void
151 | {
152 | while ($this->closers[$generator] !== []) {
153 | $this->writeNextCloser($generator);
154 | }
155 | }
156 |
157 | /**
158 | * Write the next pending closer.
159 | */
160 | protected function writeNextCloser(Generator $generator): void
161 | {
162 | if ($this->closers[$generator] !== []) {
163 | $closers = $this->closers[$generator];
164 |
165 | $this->write($generator, ...array_pop($closers));
166 |
167 | $this->closers[$generator] = $closers;
168 | }
169 | }
170 |
171 | /**
172 | * Write the node's content for the generator.
173 | */
174 | protected function write(Generator $generator, string|Slotable $content, Node $node): void
175 | {
176 | if ($content === '') {
177 | return;
178 | }
179 |
180 | if (is_string($content)) {
181 | $this->streams[$generator]->write($content);
182 |
183 | return;
184 | }
185 |
186 | $this->write($generator, $content->before(), $node);
187 |
188 | if ($node->isSelfClosing) {
189 | $this->write($generator, $content->after(), $node);
190 | } else {
191 | $this->closers[$generator] = [
192 | ...$this->closers[$generator],
193 | [$content->after(), $node],
194 | ];
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/Render/Factory.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | protected array $resolverCache = [];
21 |
22 | /**
23 | * Create a new instance.
24 | */
25 | public function __construct(
26 | protected Configuration $config,
27 | protected Highlighter $highlighter,
28 | protected Replacer $replacer,
29 | protected Container $container,
30 | ) {
31 | //
32 | }
33 |
34 | /**
35 | * Make a HTML string.
36 | */
37 | public function html(string $content): HtmlString
38 | {
39 | return new HtmlString($content);
40 | }
41 |
42 | /**
43 | * Make a HTML tag.
44 | *
45 | * @param array> $attributes
46 | * @param list|string $class
47 | */
48 | public function tag(
49 | string $as,
50 | array $attributes = [],
51 | array|string $class = [],
52 | string|Stringable $before = '',
53 | string|Stringable $after = '',
54 | ?Slotable $slot = null,
55 | ): HtmlTag {
56 | return new HtmlTag($as, $attributes, $class, $before, $after, $slot);
57 | }
58 |
59 | /**
60 | * Make a code snippet.
61 | */
62 | public function codeSnippet(string $snippet, string $language): string
63 | {
64 | return array_reduce([
65 | $this->replacer->handle(...),
66 | $this->highlighter->handle(...),
67 | ], fn ($snippet, $f) => $f($snippet, $language), $snippet);
68 | }
69 |
70 | /**
71 | * Make inline text.
72 | */
73 | public function inlineText(string $before = '', string $after = ''): Wrapper
74 | {
75 | return $this->wrapper(
76 | before: ' '.ltrim($before, ' '),
77 | after: $after,
78 | );
79 | }
80 |
81 | /**
82 | * Make a wrapper.
83 | */
84 | public function wrapper(string|Stringable $before = '', string|Stringable $after = '', ?Slotable $slot = null): Wrapper
85 | {
86 | return new Wrapper($before, $after, $slot);
87 | }
88 |
89 | /**
90 | * Make a component.
91 | *
92 | * @param array $data
93 | */
94 | public function component(string|Closure $component, array $data = []): Slotable
95 | {
96 | return with($this->resolve($component, $data), fn (Slotable|string $component) => $component instanceof Slotable
97 | ? $component
98 | : new HtmlString($component));
99 | }
100 |
101 | public function export(mixed $value): string
102 | {
103 | return var_export($value, true);
104 | }
105 |
106 | /**
107 | * Resolve the component.
108 | *
109 | * @param array $data
110 | */
111 | protected function resolve(string|Closure $component, array $data): Slotable|string
112 | {
113 | return $this->container->call(
114 | $component instanceof Closure ? $component : $this->resolver($component),
115 | $data,
116 | );
117 | }
118 |
119 | /**
120 | * The component resolver.
121 | */
122 | protected function resolver(string $path): Closure
123 | {
124 | return $this->resolverCache[$path] ??= (static fn (string $__path) => require_once $__path)(
125 | "{$this->config->get('component_directory')}/{$path}.php"
126 | );
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Render/Highlighter.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | protected Collection $filesCache;
22 |
23 | /**
24 | * Create a new instance.
25 | */
26 | public function __construct(
27 | protected Shiki $shiki,
28 | protected Configuration $config,
29 | protected Finder $finder,
30 | ) {
31 | //
32 | }
33 |
34 | /**
35 | * Highlight the given code snippet.
36 | */
37 | public function handle(string $code, string $language): string
38 | {
39 | if ($this->files()->contains($path = $this->path($code, $language))) {
40 | return file_get_contents($path);
41 | }
42 |
43 | return tap($this->shiki->highlightCode($code, match ($language) {
44 | 'php' => 'blade',
45 | 'apache-conf' => 'apache',
46 | 'nginx-conf' => 'nginx',
47 | default => $language,
48 | }), fn ($content) => file_put_contents($path, $content));
49 | }
50 |
51 | /**
52 | * The cache path for the given code snippet.
53 | */
54 | protected function path(string $code, string $language): string
55 | {
56 | return "{$this->config->get('cache_directory')}/{$this->hash($code, $language)}-highlight.{$language}.html";
57 | }
58 |
59 | /**
60 | * The hash for the given code snippet.
61 | */
62 | protected function hash(string $code, string $language): string
63 | {
64 | return hash('xxh128', "{$language}\n{$code}");
65 | }
66 |
67 | /**
68 | * The cached highlighted files.
69 | *
70 | * @return Collection
71 | */
72 | protected function files(): Collection
73 | {
74 | return $this->filesCache ??= collect($this->finder
75 | ->in($this->config->get('cache_directory'))
76 | ->files()
77 | ->depth(0)
78 | ->name('*-highlight.*.html')
79 | ->getIterator())->keys();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Render/HtmlString.php:
--------------------------------------------------------------------------------
1 | content;
27 | }
28 |
29 | /**
30 | * The content after the main content.
31 | */
32 | public function after(): string
33 | {
34 | return '';
35 | }
36 |
37 | /**
38 | * Convert to a string.
39 | */
40 | public function toString(): string
41 | {
42 | return $this->before().$this->after();
43 | }
44 |
45 | /**
46 | * Convert to a string.
47 | */
48 | public function __toString(): string
49 | {
50 | return $this->toString();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Render/HtmlTag.php:
--------------------------------------------------------------------------------
1 | > $attributes
17 | * @param list|string $class
18 | */
19 | public function __construct(
20 | protected string $as,
21 | protected array $attributes,
22 | protected array|string $class,
23 | protected string|Stringable $before,
24 | protected string|Stringable $after,
25 | protected ?Slotable $slot,
26 | ) {
27 | //
28 | }
29 |
30 | /**
31 | * Retrieve the "before" content.
32 | */
33 | public function before(): string
34 | {
35 | if ($this->isVoidTag()) {
36 | return "<{$this->as}{$this->attributeList()}>";
37 | }
38 |
39 | return "<{$this->as}{$this->attributeList()}>{$this->before}{$this->slot?->before()}";
40 | }
41 |
42 | /**
43 | * Retrieve the "after" content.
44 | */
45 | public function after(): string
46 | {
47 | if ($this->isVoidTag()) {
48 | return '';
49 | }
50 |
51 | return "{$this->slot?->after()}{$this->after}{$this->as}>";
52 | }
53 |
54 | /**
55 | * Retrieve the tags attribute list.
56 | */
57 | protected function attributeList(): string
58 | {
59 | return with($this->attributes(), fn (array $attributes) => $attributes === []
60 | ? ''
61 | : ' '.implode(' ', $attributes));
62 | }
63 |
64 | /**
65 | * Retrieve the tags attributes.
66 | *
67 | * @return list
68 | */
69 | protected function attributes(): array
70 | {
71 | $attributes = array_merge($this->attributes, [
72 | 'class' => $this->class ?: false,
73 | ]);
74 |
75 | $attributes = array_map(function (string|array|bool $value, string $key) {
76 | if ($value === false) {
77 | return false;
78 | }
79 |
80 | $key = trim($key);
81 |
82 | if ($value === true) {
83 | return $key;
84 | }
85 |
86 | if (! is_array($value)) {
87 | $value = explode(' ', $value);
88 | }
89 |
90 | $value = array_map(trim(...), $value);
91 |
92 | return $key.'="'.implode(' ', $value).'"';
93 | }, $attributes, array_keys($attributes));
94 |
95 | $attributes = array_filter($attributes, fn (string|false $value) => $value !== false);
96 |
97 | return array_values($attributes);
98 | }
99 |
100 | /**
101 | * Attach the given attributes.
102 | *
103 | * @param array> $attributes
104 | */
105 | public function withAttributes(array $attributes): HtmlTag
106 | {
107 | return new HtmlTag(
108 | as: $this->as,
109 | attributes: array_merge_recursive($this->attributes, $attributes),
110 | class: $this->class,
111 | before: $this->before,
112 | after: $this->after,
113 | slot: $this->slot,
114 | );
115 | }
116 |
117 | /**
118 | * Wrap the given slot.
119 | */
120 | public function wrapSlot(Slotable $slot): HtmlTag
121 | {
122 | return new HtmlTag(
123 | as: $this->as,
124 | attributes: $this->attributes,
125 | class: $this->class,
126 | before: $this->before,
127 | after: $this->after,
128 | slot: $slot,
129 | );
130 | }
131 |
132 | /**
133 | * Convert to a string.
134 | */
135 | public function toString(): string
136 | {
137 | if ($this->slot !== null) {
138 | throw new RuntimeException('Unable to render a tag with a content wrapper.');
139 | }
140 |
141 | return $this->before().$this->after();
142 | }
143 |
144 | /**
145 | * Convert to a string.
146 | */
147 | public function __toString(): string
148 | {
149 | return $this->toString();
150 | }
151 |
152 | /**
153 | * Determine if the tag is considered a void tag that does not need closing.
154 | */
155 | protected function isVoidTag(): bool
156 | {
157 | return in_array($this->as, [
158 | 'area',
159 | 'base',
160 | 'br',
161 | 'col',
162 | 'embed',
163 | 'hr',
164 | 'img',
165 | 'input',
166 | 'link',
167 | 'meta',
168 | 'source',
169 | 'track',
170 | 'wbr',
171 | ], true);
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/Render/Replacer.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | protected Collection $filesCache;
21 |
22 | /**
23 | * Create a new instance.
24 | */
25 | public function __construct(
26 | protected Configuration $config,
27 | protected Finder $finder,
28 | ) {
29 | //
30 | }
31 |
32 | /**
33 | * Replace the given code snippet.
34 | */
35 | public function handle(string $code, string $language): string
36 | {
37 | if ($this->files()->doesntContain($path = $this->originalPath($code, $language))) {
38 | file_put_contents($path, $code);
39 | }
40 |
41 | if ($this->files()->contains($path = $this->replacementPath($code, $language))) {
42 | return file_get_contents($path);
43 | }
44 |
45 | return $code;
46 | }
47 |
48 | /**
49 | * The cache path for the original code snippet.
50 | */
51 | protected function originalPath(string $code, string $language): string
52 | {
53 | return "{$this->config->get('cache_directory')}/{$this->hash($code, $language)}-original.{$language}";
54 | }
55 |
56 | /**
57 | * The cache path for replacement code snippet.
58 | */
59 | protected function replacementPath(string $code, string $language): string
60 | {
61 | // TODO. these should not be in the cache path and instead in a commited repository.
62 | // Maybe we should commit all code snippets to track what is modified?
63 | return "{$this->config->get('cache_directory')}/{$this->hash($code, $language)}-replacement.{$language}";
64 | }
65 |
66 | /**
67 | * The hash for the given code snippet.
68 | */
69 | protected function hash(string $code, string $language): string
70 | {
71 | return hash('xxh128', "{$language}\n{$code}");
72 | }
73 |
74 | /**
75 | * The cached replacement and original files.
76 | *
77 | * @return Collection
78 | */
79 | protected function files(): Collection
80 | {
81 | return $this->filesCache ??= collect($this->finder
82 | ->in($this->config->get('cache_directory'))
83 | ->files()
84 | ->depth(0)
85 | ->name(['*-replacement.*', '*-original.*'])
86 | ->getIterator())->keys();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Render/Wrapper.php:
--------------------------------------------------------------------------------
1 | before.$this->slot?->before();
30 | }
31 |
32 | /**
33 | * Retrieve the "after" content.
34 | */
35 | public function after(): string
36 | {
37 | return $this->slot?->after().$this->after;
38 | }
39 |
40 | /**
41 | * Convert to a string.
42 | */
43 | public function toString(): string
44 | {
45 | if ($this->slot !== null) {
46 | throw new RuntimeException('Unable to render a wrapper with a content wrapper.');
47 | }
48 |
49 | return $this->before().$this->after();
50 | }
51 |
52 | /**
53 | * Convert to a string.
54 | */
55 | public function __toString(): string
56 | {
57 | return $this->toString();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Stream/FileStreamFactory.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | protected array $buffer = [];
20 |
21 | /**
22 | * Make a new file stream.
23 | */
24 | public function make(string $path, int $chunk): Stream
25 | {
26 | return new Stream(
27 | $path,
28 | function (string $path) use ($chunk) {
29 | if (! file_exists(dirname($path))) {
30 | mkdir(dirname($path), recursive: true);
31 | }
32 |
33 | return with(fopen($path, 'w'), fn ($file) => [
34 | function (string $content) use ($file, $chunk) {
35 | $this->buffer[] = $content;
36 |
37 | if (count($this->buffer) >= $chunk) {
38 | $this->flushBuffer($file);
39 | }
40 | },
41 | function () use ($file) {
42 | $this->flushBuffer($file);
43 |
44 | fclose($file);
45 | },
46 | ]);
47 | },
48 | );
49 | }
50 |
51 | /**
52 | * Flush the buffer to the stream.
53 | *
54 | * @param resource $file
55 | */
56 | protected function flushBuffer($file): void
57 | {
58 | if ($this->buffer === []) {
59 | return;
60 | }
61 |
62 | fwrite($file, implode('', $this->buffer));
63 |
64 | $this->buffer = [];
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Stream/State.php:
--------------------------------------------------------------------------------
1 | state === State::Closed) {
43 | throw new RuntimeException('Unable to write. Stream is closed.');
44 | }
45 |
46 | if ($this->state === State::Unopened) {
47 | [$this->write, $this->close] = ($this->open)($this->path);
48 |
49 | $this->state = State::Open;
50 | }
51 |
52 | assert($this->write !== null);
53 |
54 | ($this->write)($content);
55 |
56 | return $this;
57 | }
58 |
59 | /**
60 | * Close the stream.
61 | */
62 | public function close(): void
63 | {
64 | if ($this->state === State::Closed) {
65 | throw new RuntimeException('Stream has already been closed.');
66 | }
67 |
68 | if ($this->state === State::Open) {
69 | assert($this->close !== null);
70 |
71 | ($this->close)();
72 |
73 | $this->write = $this->close = null;
74 | }
75 |
76 | $this->state = State::Closed;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Support/Link.php:
--------------------------------------------------------------------------------
1 | destination, '#');
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Support/Translator.php:
--------------------------------------------------------------------------------
1 | $replace
13 | */
14 | public function get($key, array $replace = [], $locale = null, $fallback = true): string
15 | {
16 | $value = parent::get(...func_get_args());
17 |
18 | assert(is_string($value));
19 |
20 | return $value;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Website/EmptyChunkIndex.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | protected ?Collection $idsCache;
29 |
30 | /**
31 | * Indicates that the current chunk is empty.
32 | */
33 | protected bool $isEmpty = true;
34 |
35 | /**
36 | * The current chunk.
37 | */
38 | protected ?Node $chunk = null;
39 |
40 | /**
41 | * Create a new instance.
42 | */
43 | public function __construct(
44 | protected FileStreamFactory $streamFactory,
45 | protected Configuration $config,
46 | protected Factory $render,
47 | ) {
48 | $this->stream = $this->streamFactory->make(
49 | "{$this->config->get('index_directory')}/website/{$this->config->get('language')}/empty_pages.php",
50 | 1000,
51 | );
52 | }
53 |
54 | /**
55 | * Set up.
56 | */
57 | public function setUp(): void
58 | {
59 | $this->stream->write(<<<'PHP'
60 | stream;
75 | }
76 |
77 | /**
78 | * Determine if the generator should chunk.
79 | */
80 | public function shouldChunk(Node $node): bool
81 | {
82 | return false;
83 | }
84 |
85 | /**
86 | * Render the given node.
87 | */
88 | public function render(Node $node): string|Slotable
89 | {
90 | if ($node->wantsToChunk()) {
91 | return with($this->emptyChunk(), function (?Node $emptyChunk) use ($node) {
92 | $this->chunk = $node;
93 | $this->isEmpty = true;
94 |
95 | return $emptyChunk !== null
96 | ? " {$this->render->export($emptyChunk->id())},\n"
97 | : '';
98 | });
99 | }
100 |
101 | if (! in_array($node->name, ['info', 'title', '#text'], true)) {
102 | $this->isEmpty = false;
103 | }
104 |
105 | return '';
106 | }
107 |
108 | /**
109 | * Tear down.
110 | */
111 | public function tearDown(): void
112 | {
113 | $this->stream->write('];');
114 | }
115 |
116 | /**
117 | * Retrieve the empty chunk IDs.
118 | *
119 | * @return Collection
120 | */
121 | public function ids(): Collection
122 | {
123 | return $this->idsCache ??= collect(require $this->stream->path); // @phpstan-ignore argument.templateType, argument.templateType
124 | }
125 |
126 | /**
127 | * Retrieve the current empty chunk.
128 | */
129 | protected function emptyChunk(): ?Node
130 | {
131 | return $this->isEmpty
132 | ? $this->chunk
133 | : null;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/Website/FunctionIndex.php:
--------------------------------------------------------------------------------
1 | >
29 | */
30 | protected ?Collection $allCache;
31 |
32 | /**
33 | * The current parameter number.
34 | */
35 | protected int $paramNumber = 1;
36 |
37 | /**
38 | * The function description.
39 | */
40 | protected ?string $description = null;
41 |
42 | /**
43 | * Create a new instance.
44 | */
45 | public function __construct(
46 | protected Factory $render,
47 | protected FileStreamFactory $streamFactory,
48 | protected Configuration $config,
49 | ) {
50 | $this->stream = $this->streamFactory->make(
51 | "{$this->config->get('index_directory')}/website/{$this->config->get('language')}/functions.php",
52 | 1000,
53 | );
54 | }
55 |
56 | /**
57 | * Set up.
58 | */
59 | public function setUp(): void
60 | {
61 | $this->stream->write(<<<'PHP'
62 | stream;
80 | }
81 |
82 | /**
83 | * Determine if the generator should chunk.
84 | */
85 | public function shouldChunk(Node $node): bool
86 | {
87 | return false;
88 | }
89 |
90 | /**
91 | * Render the given node.
92 | */
93 | public function render(Node $node): string|Slotable
94 | {
95 | if ($node->name === 'refpurpose') {
96 | // Str::squish?
97 | $this->description = $node->innerContent();
98 | }
99 |
100 | if ($node->name === 'methodsynopsis' && $node->parent('refsect1')) {
101 | // TODO think we can reset like we do in the image index.
102 | $wrapper = $this->render->wrapper(
103 | before: " new Method(\n description:".$this->render->export($this->description).',',
104 | after: new class(fn () => ($this->paramNumber = 1)) implements Stringable
105 | {
106 | public function __construct(protected Closure $callback)
107 | {
108 | //
109 | }
110 |
111 | public function __toString(): string
112 | {
113 | ($this->callback)();
114 |
115 | return "\n ),\n\n";
116 | }
117 | },
118 | );
119 |
120 | $this->description = null;
121 |
122 | return $wrapper;
123 | }
124 |
125 | if ($node->name === 'methodparam' && $node->parent('methodsynopsis.refsect1')) {
126 | return $this->render->wrapper(
127 | before: "\n p".($this->paramNumber++).': new Parameter([',
128 | after: '),',
129 | );
130 | }
131 |
132 | if (
133 | $node->name === 'type' &&
134 | $node->parent('methodsynopsis.refsect1') &&
135 | $node->hasAttribute('class') &&
136 | $node->attribute('class') === 'union'
137 | ) {
138 | return $this->render->wrapper(
139 | before: "\n returnTypes: [",
140 | after: '],',
141 | );
142 | }
143 |
144 | if (
145 | $node->name === 'type' &&
146 | $node->parent('methodsynopsis.refsect1')
147 | ) {
148 | return $this->render->wrapper(
149 | before: "\n returnTypes: [",
150 | after: '],',
151 | );
152 | }
153 |
154 | if (
155 | $node->name === 'type' &&
156 | $node->parent('methodparam.methodsynopsis.refsect1') &&
157 | $node->hasAttribute('class') &&
158 | $node->attribute('class') === 'union'
159 | ) {
160 | return $this->render->wrapper(
161 | before: '[',
162 | after: '],',
163 | );
164 | }
165 |
166 | if ($node->name === '#text') {
167 | // return type
168 | if ($node->parent('type.methodsynopsis.refsect1') || $node->parent('type.type.methodsynopsis.refsect1')) {
169 | return "'{$node->value}',";
170 | }
171 |
172 | // method name
173 | if ($node->parent('methodname.methodsynopsis.refsect1')) {
174 | return "\n name:".$this->render->export($node->value).',';
175 | }
176 |
177 | // param type
178 | if ($node->parent('type.methodparam.methodsynopsis.refsect1') || $node->parent('type.type.methodparam.methodsynopsis.refsect1')) {
179 | return $this->render->export($node->value).',';
180 | }
181 |
182 | // param name
183 | if ($node->parent('parameter.methodparam.methodsynopsis.refsect1')) {
184 | return '],'.$this->render->export($node->value);
185 | }
186 | }
187 |
188 | return '';
189 | }
190 |
191 | /**
192 | * Tear down.
193 | */
194 | public function tearDown(): void
195 | {
196 | $this->stream->write('];');
197 | }
198 |
199 | /**
200 | * Retrieve all functions from the index.
201 | *
202 | * @return Collection>
203 | */
204 | public function all(): Collection
205 | {
206 | return $this->allCache ??= collect(require $this->stream->path) // @phpstan-ignore argument.templateType, argument.templateType, assign.propertyType, return.type
207 | ->reduce(function ($carry, $function) {
208 | $carry[$function->name] = collect([
209 | ...($carry[$function->name] ?? []),
210 | $function,
211 | ]);
212 |
213 | return $carry;
214 | }, collect([]));
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/Website/Generator.php:
--------------------------------------------------------------------------------
1 | config->get('build_directory')}/{$node->id()}.html",
68 | fn (string $path) => with($this->streamFactory->make($path, 1000), fn (Stream $stream) => [
69 | $stream->write(file_get_contents('resources/header.html'))
70 | ->write($this->render->component('main')->before())
71 | ->write(...),
72 | fn () => $stream->write($this->render->component('main')->after())
73 | ->write($this->render->component('menu', [
74 | 'active' => $this->titleIndex->find($node->id()),
75 | 'items' => $this->titleIndex->heirachy(),
76 | 'empty' => $this->titleIndex->findMany(
77 | $this->emptyChunkIndex->ids(),
78 | ),
79 | ])->toString())
80 | ->write(file_get_contents('resources/footer.html'))
81 | ->close(),
82 | ]),
83 | );
84 | }
85 |
86 | /**
87 | * Determine if the generator should chunk.
88 | */
89 | public function shouldChunk(Node $node): bool
90 | {
91 | return $node->wantsToChunk();
92 | }
93 |
94 | /**
95 | * Render the given node.
96 | */
97 | public function render(Node $node): Slotable|string
98 | {
99 | return with(match ($node->name) {
100 | '#cdata-section' => $this->renderCData($node),
101 | '#text' => $this->renderText($node),
102 | 'abbrev' => $this->renderAbbrev($node),
103 | 'abstract' => $this->renderAbstract($node),
104 | 'acronym' => $this->renderAcronym($node),
105 | 'alt' => $this->renderAlt($node),
106 | 'author' => $this->renderAuthor($node),
107 | 'authorgroup' => $this->renderAuthorGroup($node),
108 | 'book' => $this->renderBook($node),
109 | 'caption' => $this->renderCaption($node),
110 | 'caution' => $this->renderCaution($node),
111 | 'chapter' => $this->renderChapter($node),
112 | 'classname' => $this->renderClassName($node),
113 | 'code' => $this->renderCode($node),
114 | 'command' => $this->renderCommand($node),
115 | 'computeroutput' => $this->renderComputerOutput($node),
116 | 'constant' => $this->renderConstant($node),
117 | 'copyright' => $this->renderCopyright($node),
118 | 'dbtimestamp' => $this->renderDbTimestamp($node),
119 | 'editor' => $this->renderEditor($node),
120 | 'emphasis' => $this->renderEmphasis($node),
121 | 'entry' => $this->renderEntry($node),
122 | 'envar' => $this->renderEnVar($node),
123 | 'example' => $this->renderExample($node),
124 | 'filename' => $this->renderFilename($node),
125 | 'firstname' => $this->renderFirstName($node),
126 | 'function' => $this->renderFunction($node),
127 | 'holder' => $this->renderHolder($node),
128 | 'imagedata' => $this->renderImageData($node),
129 | 'imageobject' => $this->renderImageObject($node),
130 | 'info' => $this->renderInfo($node),
131 | 'informalexample' => $this->renderInformalExample($node),
132 | 'informaltable' => $this->renderInformalTable($node),
133 | 'interfacename' => $this->renderInterfaceName($node),
134 | 'itemizedlist' => $this->renderItemizedList($node),
135 | 'legalnotice' => $this->renderLegalNotice($node),
136 | 'link' => $this->renderLink($node),
137 | 'listitem' => $this->renderListItem($node),
138 | 'literal' => $this->renderLiteral($node),
139 | 'literallayout' => $this->renderLiteralLayout($node),
140 | 'mediaobject' => $this->renderMediaObject($node),
141 | 'member' => $this->renderMember($node),
142 | 'methodname' => $this->renderMethodName($node),
143 | 'modifier' => $this->renderModifier($node),
144 | 'note' => $this->renderNote($node),
145 | 'option' => $this->renderOption($node),
146 | 'optional' => $this->renderOptional($node),
147 | 'orderedlist' => $this->renderOrderedList($node),
148 | 'othercredit' => $this->renderOtherCredit($node),
149 | 'othername' => $this->renderOtherName($node),
150 | 'para' => $this->renderPara($node),
151 | 'parameter' => $this->renderParameter($node),
152 | 'personname' => $this->renderPersonName($node),
153 | 'phpdoc' => $this->renderPhpDoc($node),
154 | 'preface' => $this->renderPreface($node),
155 | 'procedure' => $this->renderProcedure($node),
156 | 'productname' => $this->renderProductName($node),
157 | 'programlisting' => $this->renderProgramListing($node),
158 | 'pubdate' => $this->renderPubDate($node),
159 | 'replaceable' => $this->renderReplaceable($node),
160 | 'row' => $this->renderRow($node),
161 | 'screen' => $this->renderScreen($node),
162 | 'sect1' => $this->renderSect1($node),
163 | 'sect2' => $this->renderSect2($node),
164 | 'sect3' => $this->renderSect3($node),
165 | 'sect4' => $this->renderSect4($node),
166 | 'section' => $this->renderSection($node),
167 | 'set' => $this->renderSet($node),
168 | 'simpara' => $this->renderSimPara($node),
169 | 'simplelist' => $this->renderSimpleList($node),
170 | 'step' => $this->renderStep($node),
171 | 'surname' => $this->renderSurname($node),
172 | 'synopsis' => $this->renderSynopsis($node),
173 | 'systemitem' => $this->renderSystemItem($node),
174 | 'table' => $this->renderTable($node),
175 | 'tbody' => $this->renderTBody($node),
176 | 'term' => $this->renderTerm($node),
177 | 'tgroup' => $this->renderTGroup($node),
178 | 'thead' => $this->renderTHead($node),
179 | 'tip' => $this->renderTip($node),
180 | 'title' => $this->renderTitle($node),
181 | 'titleabbrev' => $this->renderTitleAbbrev($node),
182 | 'type' => $this->renderType($node),
183 | 'userinput' => $this->renderUserInput($node),
184 | 'variablelist' => $this->renderVariableList($node),
185 | 'varlistentry' => $this->renderVarListEntry($node),
186 | 'varname' => $this->renderVarName($node),
187 | 'warning' => $this->renderWarning($node),
188 | 'xref' => $this->renderXref($node),
189 | 'year' => $this->renderYear($node),
190 | default => tap('', fn () => dd('Unknown node', $node->name, $node->lineage())),
191 | }, fn (string|Slotable $content) => $this->config->get('debug')
192 | ? $this->withDebuggingInfo($node, $content)
193 | : $content);
194 | }
195 |
196 | /**
197 | * Tear down.
198 | */
199 | public function tearDown(): void
200 | {
201 | $this->output->line(<<<'BASH'
202 | Building assets.
203 | BASH);
204 |
205 | system(<<<'BASH'
206 | npm run build
207 | npx tailwindcss -i ./resources/style.css -o ./build/output/style.css
208 | cp ./resources/script.js ./build/output/script.js
209 | BASH);
210 | }
211 |
212 | /**
213 | * Retrieve the generator's indexes.
214 | *
215 | * @return list
216 | */
217 | public function indexes(): array
218 | {
219 | return [
220 | $this->titleIndex,
221 | $this->imageIndex,
222 | $this->functionIndex,
223 | $this->emptyChunkIndex,
224 | ];
225 | }
226 |
227 | /**
228 | * Render the CDATA node.
229 | */
230 | protected function renderCData(Node $node): Slotable|string
231 | {
232 | $content = preg_replace('/^\\n/', '', $node->value);
233 |
234 | /**
235 | * A literal listing of all or part of a program.
236 | *
237 | * @see self::renderProgramListing()
238 | */
239 | if ($programlisting = $node->parent('programlisting')) {
240 | if ($programlisting->hasRole()) {
241 | return $this->render->codeSnippet($content, $programlisting->role());
242 | }
243 |
244 | return $content;
245 | }
246 |
247 | /**
248 | * Text that a user sees or might see on a computer screen.
249 | *
250 | * @see self::renderScreen()
251 | */
252 | if ($screen = $node->parent('screen')) {
253 | if ($screen->hasRole()) {
254 | return $this->render->codeSnippet($content, $screen->role());
255 | }
256 |
257 | return $content;
258 | }
259 |
260 | return e($content);
261 | }
262 |
263 | /**
264 | * Render the text node.
265 | */
266 | protected function renderText(Node $node): Slotable|string
267 | {
268 | /**
269 | * Text that a user sees or might see on a computer screen.
270 | *
271 | * @see self::renderScreen()
272 | */
273 | if ($screen = $node->ancestor('screen')) {
274 | if ($screen->hasRole()) {
275 | return $this->render->codeSnippet($node->value, $screen->role());
276 | }
277 |
278 | return $node->value;
279 | }
280 |
281 | return e($node->value);
282 | }
283 |
284 | /**
285 | * An abbreviation, especially one followed by a period.
286 | *
287 | * @see https://tdg.docbook.org/tdg/5.2/abbrev.html
288 | *
289 | * @todo It would be nice if these had a "title" tag for accessiblity. We
290 | * will likely need more indexers for this.
291 | */
292 | protected function renderAbbrev(Node $node): Slotable|string
293 | {
294 | return $this->render->tag('abbr');
295 | }
296 |
297 | /**
298 | * A summary.
299 | *
300 | * @see https://tdg.docbook.org/tdg/5.2/abstract.html
301 | */
302 | protected function renderAbstract(Node $node): Slotable|string
303 | {
304 | return '';
305 | }
306 |
307 | /**
308 | * An often pronounceable word made from the initial (or selected) letters of a name or phrase.
309 | *
310 | * @see https://tdg.docbook.org/tdg/5.2/acronym.html
311 | *
312 | * @todo It would be nice if these had a "title" tag for accessiblity. We
313 | * will likely need more indexers for this.
314 | */
315 | protected function renderAcronym(Node $node): Slotable|string
316 | {
317 | return $this->render->tag('abbr');
318 | }
319 |
320 | /**
321 | * A text-only annotation, often used for accessibility.
322 | *
323 | * @see https://tdg.docbook.org/tdg/5.2/alt.html
324 | *
325 | * @todo improve this
326 | */
327 | protected function renderAlt(Node $node): Slotable|string
328 | {
329 | return '';
330 | }
331 |
332 | /**
333 | * The name of an individual author.
334 | *
335 | * This tag only appears at he beginning of the documentation to credit the
336 | * authors, so it does not need to be generic and handle any situation.
337 | *
338 | * @see https://tdg.docbook.org/tdg/5.2/author.html
339 | * @see self::renderAuthorGroup()
340 | */
341 | protected function renderAuthor(Node $node): Slotable|string
342 | {
343 | $authorgroup = $node->expectParent('authorgroup');
344 |
345 | if ($authorgroup->id() === 'authors') {
346 | return $this->render->tag('li');
347 | }
348 |
349 | if ($authorgroup->id() === 'editors') {
350 | return '';
351 | }
352 |
353 | $this->unhandledNode($node, 'Unknown parent ID for "authorgroup".');
354 | }
355 |
356 | /**
357 | * A book.
358 | *
359 | * @see https://tdg.docbook.org/tdg/5.2/book.html
360 | */
361 | protected function renderBook(Node $node): Slotable|string
362 | {
363 | return '';
364 | }
365 |
366 | /**
367 | * A caption.
368 | *
369 | * @see https://tdg.docbook.org/tdg/5.2/caption.html
370 | */
371 | protected function renderCaption(Node $node): Slotable|string
372 | {
373 | // TODO this needs to be included in the image indexer and should be rendered
374 | // as a figurecaption.
375 | return '';
376 | }
377 |
378 | /**
379 | * The name of a class, in the object-oriented programming sense.
380 | *
381 | * @see https://tdg.docbook.org/tdg/5.2/classname.html
382 | */
383 | protected function renderClassName(Node $node): Slotable|string
384 | {
385 | $link = $this->render->component('link', [
386 | 'link' => Link::internal("class.{$node->innerContent()}"),
387 | ]);
388 |
389 | return $this->render->component('inline-code')->wrapSlot($link);
390 | }
391 |
392 | /**
393 | * Wrapper for author information when a document has multiple authors or
394 | * collaborators.
395 | *
396 | * This tag only appears at he beginning of the documentation to credit the
397 | * authors, so it does not need to be generic and handle any situation.
398 | *
399 | * @see https://tdg.docbook.org/tdg/5.2/authorgroup.html
400 | */
401 | protected function renderAuthorGroup(Node $node): Slotable|string
402 | {
403 | if ($node->id() === 'authors') {
404 | return $this->render->component('authors');
405 | }
406 |
407 | if ($node->id() === 'editors') {
408 | return '';
409 | }
410 |
411 | $this->unhandledNode($node, 'Unknown ID for "authorgroup".');
412 | }
413 |
414 | /**
415 | * A note of caution.
416 | *
417 | * @see https://tdg.docbook.org/tdg/5.2/caution.html
418 | */
419 | protected function renderCaution(Node $node): Slotable|string
420 | {
421 | return $this->render->component('caution');
422 | }
423 |
424 | /**
425 | * A chapter, as of a book.
426 | *
427 | * @see https://tdg.docbook.org/tdg/5.2/chapter.html
428 | */
429 | protected function renderChapter(Node $node): Slotable|string
430 | {
431 | return '';
432 | }
433 |
434 | /**
435 | * An inline code fragment.
436 | *
437 | * @see https://tdg.docbook.org/tdg/5.2/code.html
438 | */
439 | protected function renderCode(Node $node): Slotable|string
440 | {
441 | return $this->render->component('inline-code');
442 | }
443 |
444 | /**
445 | * The name of an executable program or other software command.
446 | *
447 | * @see https://tdg.docbook.org/tdg/5.2/command.html
448 | */
449 | protected function renderCommand(Node $node): Slotable|string
450 | {
451 | return $this->render->component('inline-code');
452 | }
453 |
454 | /**
455 | * Data, generally text, displayed or presented by a computer.
456 | *
457 | * @see https://tdg.docbook.org/tdg/5.2/computeroutput.html
458 | */
459 | protected function renderComputerOutput(Node $node): Slotable|string
460 | {
461 | return $this->render->component('inline-code');
462 | }
463 |
464 | /**
465 | * A programming or system constant.
466 | *
467 | * @see https://tdg.docbook.org/tdg/5.2/constant.html
468 | */
469 | protected function renderConstant(Node $node): Slotable|string
470 | {
471 | if ($node->hasParent('title')) {
472 | return $this->render->tag(
473 | as: 'var',
474 | class: 'font-mono not-italic',
475 | );
476 | }
477 |
478 | return $this->render->component('inline-code', [
479 | 'as' => 'var',
480 | ]);
481 | }
482 |
483 | /**
484 | * Copyright information about a document.
485 | *
486 | * @see https://tdg.docbook.org/tdg/5.2/copyright.html
487 | */
488 | protected function renderCopyright(Node $node): Slotable|string
489 | {
490 | return ' © ';
491 | }
492 |
493 | /**
494 | * A timestamp processing instruction.
495 | *
496 | * @see http://www.sagehill.net/docbookxsl/Datetime.html
497 | * @see https://www.php.net/manual/en/datetime.format.php
498 | */
499 | protected function renderDbTimestamp(Node $node): Slotable|string
500 | {
501 | preg_match('/.*?format="(.*?)"/', $node->value, $matches);
502 |
503 | return with(new DateTimeImmutable, fn (DateTimeImmutable $now) => collect(mb_str_split($matches[1]))
504 | ->map(fn (string $component) => match ($component) {
505 | 'a' => $now->format('D'),
506 | 'A' => $now->format('l'),
507 | 'b' => $now->format('M'),
508 | 'c' => $now->format('c'),
509 | 'B' => $now->format('F'),
510 | 'd' => str_contains($node->value, ' padding="0"')
511 | ? ltrim($now->format('d'), '0')
512 | : $now->format('d'),
513 | 'H' => str_contains($node->value, ' padding="0"')
514 | ? ltrim($now->format('H'), '0')
515 | : $now->format('H'),
516 | 'j' => $now->format('z'),
517 | 'm' => str_contains($node->value, ' padding="0"')
518 | ? ltrim($now->format('m'), '0')
519 | : $now->format('m'),
520 | 'M' => str_contains($node->value, ' padding="0"')
521 | ? ltrim($now->format('i'), '0')
522 | : $now->format('i'),
523 | 'S' => str_contains($node->value, ' padding="0"')
524 | ? ltrim($now->format('s'), '0')
525 | : $now->format('s'),
526 | 'U' => $now->format('W'),
527 | 'w' => (string) ($now->format('w') + 1), // spec has Sunday at 1. PHP has Sunday at 0.
528 | 'x' => $now->format('Y-m-dP'),
529 | 'X' => $now->format('H:i:sP'),
530 | 'Y' => $now->format('Y'),
531 | default => $component,
532 | })->pipe(fn (Collection $components) => $this->render->tag(
533 | as: 'time',
534 | before: $components->implode(''),
535 | attributes: [
536 | 'datetime' => $now->format('c'),
537 | ],
538 | )->toString()));
539 | }
540 |
541 | /**
542 | * The name of the editor of a document.
543 | *
544 | * This tag only appears at he beginning of the documentation to credit the
545 | * authors, so it does not need to be generic and handle any situation.
546 | *
547 | * @see https://tdg.docbook.org/tdg/5.2/editor.html
548 | */
549 | protected function renderEditor(Node $node): Slotable|string
550 | {
551 | return $this->render->component('editors');
552 | }
553 |
554 | /**
555 | * Emphasized text.
556 | *
557 | * @see https://tdg.docbook.org/tdg/5.2/emphasis.html
558 | */
559 | protected function renderEmphasis(Node $node): Slotable|string
560 | {
561 | return $this->render->tag('em');
562 | }
563 |
564 | /**
565 | * A cell in a table.
566 | *
567 | * @see https://tdg.docbook.org/tdg/5.2/entry.html
568 | */
569 | protected function renderEntry(Node $node): Slotable|string
570 | {
571 | return $this->render->tag(
572 | as: $node->hasParent('row.thead') ? 'th' : 'td',
573 | class: 'py-2 px-3 text-left first-of-type:pl-6 last:pr-6 tabular-nums',
574 | );
575 | }
576 |
577 | /**
578 | * A software environment variable.
579 | *
580 | * @see https://tdg.docbook.org/tdg/5.2/envar.html
581 | */
582 | protected function renderEnVar(Node $node): Slotable|string
583 | {
584 | return $this->render->component('inline-code', [
585 | 'as' => 'var',
586 | ]);
587 | }
588 |
589 | /**
590 | * A formal example, with a title.
591 | *
592 | * @see https://tdg.docbook.org/tdg/5.2/example.html
593 | *
594 | * @todo I think this needs improving. It might be a figure, but any
595 | * children might need to know about the figure and be figcaption or
596 | * something.
597 | */
598 | protected function renderExample(Node $node): Slotable|string
599 | {
600 | return $this->render->tag(
601 | as: 'figure',
602 | class: 'my-6',
603 | );
604 | }
605 |
606 | /**
607 | * The name of a file.
608 | *
609 | * @see https://tdg.docbook.org/tdg/5.2/filename.html
610 | */
611 | protected function renderFilename(Node $node): Slotable|string
612 | {
613 | if ($node->hasParent('title')) {
614 | return $this->render->tag('code');
615 | }
616 |
617 | return $this->render->component('emphasised-literal');
618 | }
619 |
620 | /**
621 | * A given name of a person.
622 | *
623 | * This tag only appears at he beginning of the documentation to credit the
624 | * authors, so it does not need to be generic and handle any situation.
625 | *
626 | * @see https://tdg.docbook.org/tdg/5.2/firstname.html
627 | */
628 | protected function renderFirstName(Node $node): Slotable|string
629 | {
630 | return $this->render->inlineText();
631 | }
632 |
633 | /**
634 | * The name of a function or subroutine, as in a programming language.
635 | *
636 | * @see https://tdg.docbook.org/tdg/5.2/function.html
637 | */
638 | protected function renderFunction(Node $node): Slotable|string
639 | {
640 | $this->tooltipIndex++;
641 |
642 | // TODO handle when a function has multiple prototypes.
643 | $function = $this->functionIndex->all()[$node->innerContent()][0] ?? null;
644 |
645 | // We can't actually do this. Just a step along the way to make it work...
646 | if ($function === null) {
647 | return '';
648 | }
649 |
650 | return $this->render->wrapper(
651 | slot: $this->render->component('inline-code', [
652 | 'class' => ['pr-7 relative'],
653 | 'attributes' => [
654 | 'aria-describedby' => 'tooltip',
655 | ]])
656 | ->wrapSlot($this->render->wrapper(
657 | slot: $this->render->component('link', [
658 | 'link' => Link::internal("function.{$function->name}"),
659 | ]),
660 | after: $this->render->tag(
661 | as: 'button',
662 | class: 'flex items-center justify-center ml-2 h-full w-6 border-l border-violet-200 absolute right-0 top-0 rounded-r',
663 | attributes: [
664 | 'tooltip-target' => "tooltip-{$this->tooltipIndex}",
665 | ],
666 | before: new HtmlString(<<<'HTML'
667 |
668 |
673 | HTML),
674 | after: $this->render->component('function-popup', [
675 | 'id' => "tooltip-{$this->tooltipIndex}",
676 | 'method' => $function,
677 | ]),
678 | ),
679 | )),
680 | );
681 | }
682 |
683 | /**
684 | * Pointer to external image data.
685 | *
686 | * @see https://tdg.docbook.org/tdg/5.2/imagedata.html
687 | */
688 | protected function renderImageData(Node $node): Slotable|string
689 | {
690 | // TODO: this needs to point to a public image. We need an indexer that
691 | // publishes the files, then we can just `pull` the image when we encounter
692 | // it while generating. Should also include the `alt` text.
693 | return $this->render->tag(
694 | as: 'img',
695 | attributes: [
696 | 'src' => $node->attribute('fileref'),
697 | ],
698 | );
699 | }
700 |
701 | /**
702 | * A wrapper for image data and its associated meta-information.
703 | *
704 | * @see https://tdg.docbook.org/tdg/5.2/imageobject.html
705 | */
706 | protected function renderImageObject(Node $node): Slotable|string
707 | {
708 | return '';
709 | }
710 |
711 | /**
712 | * The name of the individual or organization that holds a copyright.
713 | *
714 | * This tag is only used on the homepage to highlight the "PHP
715 | * Documentation Group"
716 | *
717 | * @see https://tdg.docbook.org/tdg/5.2/holder.html
718 | * @see self::copyright()
719 | */
720 | protected function renderHolder(Node $node): Slotable|string
721 | {
722 | return $this->render->inlineText(after: '.');
723 | }
724 |
725 | /**
726 | * A wrapper for information about a component or other block.
727 | *
728 | * @see https://tdg.docbook.org/tdg/5.2/info.html
729 | */
730 | protected function renderInfo(Node $node): Slotable|string
731 | {
732 | return '';
733 | }
734 |
735 | /**
736 | * A displayed example without a title.
737 | *
738 | * @todo I think this needs improving. It might be a figure, but any
739 | * children might need to know about the figure and be figcaption or
740 | * something.
741 | *
742 | * @see https://tdg.docbook.org/tdg/5.2/informalexample.html
743 | */
744 | protected function renderInformalExample(Node $node): Slotable|string
745 | {
746 | return $this->render->tag(
747 | as: 'figure',
748 | class: 'my-6',
749 | );
750 | }
751 |
752 | /**
753 | * An HTML table without a title.
754 | *
755 | * @see https://tdg.docbook.org/tdg/5.2/html.informaltable.html
756 | */
757 | protected function renderInformalTable(Node $node): Slotable|string
758 | {
759 | return $this->renderTable($node);
760 | }
761 |
762 | /**
763 | * The name of an interface.
764 | *
765 | * @see https://tdg.docbook.org/tdg/5.2/interfacename.html
766 | */
767 | protected function renderInterfaceName(Node $node): Slotable|string
768 | {
769 | $link = $this->render->component('link', [
770 | 'link' => Link::internal("class.{$node->innerContent()}"),
771 | ]);
772 |
773 | return $this->render->component('inline-code')->wrapSlot($link);
774 | }
775 |
776 | /**
777 | * A list in which each entry is marked with a bullet or other dingbat.
778 | *
779 | * @see https://tdg.docbook.org/tdg/5.2/itemizedlist.html
780 | */
781 | protected function renderItemizedList(Node $node): Slotable|string
782 | {
783 | return $this->render->component('unordered-list');
784 | }
785 |
786 | /**
787 | * A statement of legal obligations or requirements.
788 | *
789 | * @see https://tdg.docbook.org/tdg/5.2/legalnotice.html
790 | */
791 | protected function renderLegalNotice(Node $node): Slotable|string
792 | {
793 | return '';
794 | }
795 |
796 | /**
797 | * A hypertext link.
798 | *
799 | * @see https://tdg.docbook.org/tdg/5.2/link.html
800 | */
801 | protected function renderLink(Node $node): Slotable|string
802 | {
803 | // All titles contain "hash" links which I feel this is more valuable
804 | // than providing in-title links. We don't wanna be wrapping links
805 | // within links. That is a bad time for everyone.
806 | if ($node->hasAncestor('title')) {
807 | return '';
808 | }
809 |
810 | return $this->render->component('link', [
811 | 'link' => $node->link(),
812 | ]);
813 | }
814 |
815 | /**
816 | * A wrapper for the elements of a list item.
817 | *
818 | * @see https://tdg.docbook.org/tdg/5.2/listitem.html
819 | */
820 | protected function renderListItem(Node $node): Slotable|string
821 | {
822 | if ($node->hasParent('varlistentry')) {
823 | return $this->render->tag(
824 | as: 'dd',
825 | class: 'mt-2',
826 | );
827 | }
828 |
829 | return $this->render->tag('li');
830 | }
831 |
832 | /**
833 | * A displayed media object (video, audio, image, etc.).
834 | *
835 | * @see https://tdg.docbook.org/tdg/5.2/mediaobject.html
836 | */
837 | protected function renderMediaObject(Node $node): Slotable|string
838 | {
839 | return $this->lazy = new class($this->render) implements Wrapper {
840 | public string $alt;
841 | public string $src;
842 | public string $caption;
843 |
844 | public function __construct(
845 | private Factory $render,
846 | ) {
847 | //
848 | }
849 |
850 | public function before(): string
851 | {
852 | return '';
853 | }
854 |
855 | public function after(): string
856 | {
857 | return <<
859 |
860 | {$this->caption}
861 |
862 | HTML;
863 | }
864 | };
865 | }
866 |
867 | /**
868 | * An element of a simple list.
869 | *
870 | * @see https://tdg.docbook.org/tdg/5.2/member.html
871 | */
872 | protected function renderMember(Node $node): Slotable|string
873 | {
874 | return $this->render->tag('li');
875 | }
876 |
877 | /**
878 | * Inline text that is some literal value.
879 | *
880 | * @see https://tdg.docbook.org/tdg/5.2/literal.html
881 | */
882 | protected function renderLiteral(Node $node): Slotable|string
883 | {
884 | return $this->render->component('inline-code');
885 | }
886 |
887 | /**
888 | * A block of text in which line breaks and white space are to be reproduced faithfully.
889 | *
890 | * @see https://tdg.docbook.org/tdg/5.2/literallayout.html
891 | */
892 | protected function renderLiteralLayout(Node $node): Slotable|string
893 | {
894 | return $this->render->tag('pre');
895 | }
896 |
897 | /**
898 | * A message set off from the text.
899 | *
900 | * @see https://tdg.docbook.org/tdg/5.2/note.html
901 | */
902 | protected function renderNote(Node $node): Slotable|string
903 | {
904 | return $this->render->component('note');
905 | }
906 |
907 | /**
908 | * An option for a software command.
909 | *
910 | * @see https://tdg.docbook.org/tdg/5.2/option.html
911 | */
912 | protected function renderOption(Node $node): Slotable|string
913 | {
914 | return $this->render->component('emphasised-literal');
915 | }
916 |
917 | /**
918 | * Optional information.
919 | *
920 | * @see https://tdg.docbook.org/tdg/5.2/optional.html
921 | */
922 | protected function renderOptional(Node $node): Slotable|string
923 | {
924 | return $this->render->wrapper(
925 | before: '[',
926 | after: ']',
927 | );
928 | }
929 |
930 | /**
931 | * A list in which each entry is marked with a sequentially incremented label.
932 | *
933 | * @see https://tdg.docbook.org/tdg/5.2/orderedlist.html
934 | */
935 | protected function renderOrderedList(Node $node): Slotable|string
936 | {
937 | return $this->render->component('ordered-list', [
938 | 'type' => $node->numeration(),
939 | ]);
940 | }
941 |
942 | /**
943 | * A person or entity, other than an author or editor, credited in a document.
944 | *
945 | * This tag only appears at he beginning of the documentation to credit the
946 | * authors, so it does not need to be generic and handle any situation.
947 | *
948 | * @see https://tdg.docbook.org/tdg/5.2/othercredit.html
949 | */
950 | protected function renderOtherCredit(Node $node): Slotable|string
951 | {
952 | if ($authorGroup->id() === 'authors') {
953 | return $this->render->tag('li');
954 | }
955 |
956 | $this->unhandledNode($node, 'Unknown parent ID for othercredit.');
957 | }
958 |
959 | /**
960 | * A component of a person's name that is not a first name, surname, or lineage.
961 | *
962 | * @see https://tdg.docbook.org/tdg/5.2/othername.html
963 | */
964 | protected function renderOtherName(Node $node): Slotable|string
965 | {
966 | return '';
967 | }
968 |
969 | /**
970 | * A paragraph.
971 | *
972 | * @see https://tdg.docbook.org/tdg/5.2/para.html
973 | */
974 | protected function renderPara(Node $node): Slotable|string
975 | {
976 | // Putting paragraph tags in a `` breaks the HTML flow. We don't
977 | // need it here...as far as I can tell.
978 | if ($node->hasParent('listitem')) {
979 | return '';
980 | }
981 |
982 | return $this->render->component('paragraph');
983 | }
984 |
985 | /**
986 | * A value or a symbolic reference to a value.
987 | *
988 | * @see https://tdg.docbook.org/tdg/5.2/parameter.html
989 | */
990 | protected function renderParameter(Node $node): Slotable|string
991 | {
992 | return $this->render->component('emphasised-literal');
993 | }
994 |
995 | /**
996 | * The personal name of an individual.
997 | *
998 | * @see https://tdg.docbook.org/tdg/5.2/personname.html
999 | */
1000 | protected function renderPersonName(Node $node): Slotable|string
1001 | {
1002 | return '';
1003 | }
1004 |
1005 | /**
1006 | * Unused tag.
1007 | */
1008 | protected function renderPhpDoc(Node $node): Slotable|string
1009 | {
1010 | return '';
1011 | }
1012 |
1013 | /**
1014 | * Introductory matter preceding the first chapter of a book.
1015 | *
1016 | * @see https://tdg.docbook.org/tdg/5.2/preface.html
1017 | */
1018 | protected function renderPreface(Node $node): Slotable|string
1019 | {
1020 | return '';
1021 | }
1022 |
1023 | /**
1024 | * A list of operations to be performed in a well-defined sequence.
1025 | *
1026 | * @see https://tdg.docbook.org/tdg/5.2/procedure.html
1027 | */
1028 | protected function renderProcedure(Node $node): Slotable|string
1029 | {
1030 | return $this->render->component('ordered-list');
1031 | }
1032 |
1033 | /**
1034 | * The name of a method.
1035 | *
1036 | * @see https://tdg.docbook.org/tdg/5.2/methodname.html
1037 | */
1038 | protected function renderMethodName(Node $node): Slotable|string
1039 | {
1040 | // TODO this should show function API on hover.
1041 | // TODO this should link to the method
1042 | return $this->render->component('inline-code');
1043 | }
1044 |
1045 | /**
1046 | * Modifiers in a synopsis.
1047 | *
1048 | * @see https://tdg.docbook.org/tdg/5.2/modifier.html
1049 | */
1050 | protected function renderModifier(Node $node): Slotable|string
1051 | {
1052 | // TODO
1053 | return $this->render->component('inline-code');
1054 | }
1055 |
1056 | /**
1057 | * The formal name of a product.
1058 | *
1059 | * @see https://tdg.docbook.org/tdg/5.2/productname.html
1060 | */
1061 | protected function renderProductName(Node $node): Slotable|string
1062 | {
1063 | return '';
1064 | }
1065 |
1066 | /**
1067 | * A literal listing of all or part of a program.
1068 | *
1069 | * @see https://tdg.docbook.org/tdg/5.2/programlisting.html
1070 | */
1071 | protected function renderProgramListing(Node $node): Slotable|string
1072 | {
1073 | return $this->render->component('program-listing');
1074 | }
1075 |
1076 | /**
1077 | * The date of publication of a document.
1078 | *
1079 | * @see https://tdg.docbook.org/tdg/5.2/pubdate.html
1080 | */
1081 | protected function renderPubDate(Node $node): Slotable|string
1082 | {
1083 | if ($node->parent('info.set')?->hasNoParent()) {
1084 | return $this->render->inlineText(
1085 | before: 'Published ',
1086 | after: '.',
1087 | );
1088 | }
1089 |
1090 | $this->unhandledNode($node, 'Generic pubdate component not implemented.');
1091 | }
1092 |
1093 | /**
1094 | * Content that may or must be replaced by the user.
1095 | *
1096 | * @see https://tdg.docbook.org/tdg/5.2/replaceable.html
1097 | */
1098 | protected function renderReplaceable(Node $node): Slotable|string
1099 | {
1100 | return $this->render->wrapper(
1101 | before: '{',
1102 | after: '}',
1103 | );
1104 | }
1105 |
1106 | /**
1107 | * A row in a table.
1108 | *
1109 | * @see https://tdg.docbook.org/tdg/5.2/row.html
1110 | */
1111 | protected function renderRow(Node $node): Slotable|string
1112 | {
1113 | return $this->render->tag(
1114 | as: 'tr',
1115 | class: 'border-b border-violet-50 even:bg-violet-25',
1116 | );
1117 | }
1118 |
1119 | /**
1120 | * Text that a user sees or might see on a computer screen.
1121 | *
1122 | * @see https://tdg.docbook.org/tdg/5.2/screen.html
1123 | */
1124 | protected function renderScreen(Node $node): Slotable|string
1125 | {
1126 | return $this->render->component('screen');
1127 | }
1128 |
1129 | /**
1130 | * A top-level section of document.
1131 | *
1132 | * @see https://tdg.docbook.org/tdg/5.2/sect1.html
1133 | */
1134 | protected function renderSect1(Node $node): Slotable|string
1135 | {
1136 | return '';
1137 | }
1138 |
1139 | /**
1140 | * A subsection within a sect1.
1141 | *
1142 | * @see https://tdg.docbook.org/tdg/5.2/sect2.html
1143 | */
1144 | protected function renderSect2(Node $node): Slotable|string
1145 | {
1146 | return '';
1147 | }
1148 |
1149 | /**
1150 | * A subsection within a sect2.
1151 | *
1152 | * @see https://tdg.docbook.org/tdg/5.2/sect3.html
1153 | */
1154 | protected function renderSect3(Node $node): Slotable|string
1155 | {
1156 | return '';
1157 | }
1158 |
1159 | /**
1160 | * A subsection within a sect3.
1161 | *
1162 | * @see https://tdg.docbook.org/tdg/5.2/sect4.html
1163 | */
1164 | protected function renderSect4(Node $node): Slotable|string
1165 | {
1166 | return '';
1167 | }
1168 |
1169 | /**
1170 | * A recursive section.
1171 | *
1172 | * @see https://tdg.docbook.org/tdg/5.2/section.html
1173 | */
1174 | protected function renderSection(Node $node): Slotable|string
1175 | {
1176 | return '';
1177 | }
1178 |
1179 | /**
1180 | * A collection of books.
1181 | *
1182 | * @see https://tdg.docbook.org/tdg/5.2/set.html
1183 | */
1184 | protected function renderSet(Node $node): Slotable|string
1185 | {
1186 | return '';
1187 | }
1188 |
1189 | /**
1190 | * A paragraph that contains only text and inline markup, no block elements.
1191 | *
1192 | * @see https://tdg.docbook.org/tdg/5.2/simpara.html
1193 | */
1194 | protected function renderSimPara(Node $node): Slotable|string
1195 | {
1196 | return $this->renderPara($node);
1197 | }
1198 |
1199 | /**
1200 | * An undecorated list of single words or short phrases.
1201 | *
1202 | * @see https://tdg.docbook.org/tdg/5.2/simplelist.html
1203 | */
1204 | protected function renderSimpleList(Node $node): Slotable|string
1205 | {
1206 | return $this->render->component('unordered-list');
1207 | }
1208 |
1209 | /**
1210 | * A unit of action in a procedure.
1211 | *
1212 | * @see https://tdg.docbook.org/tdg/5.2/step.html
1213 | * @see self::renderProcedure()
1214 | */
1215 | protected function renderStep(Node $node): Slotable|string
1216 | {
1217 | return $this->render->tag('li');
1218 | }
1219 |
1220 | /**
1221 | * An inherited or family name; in western cultures the last name.
1222 | *
1223 | * This tag only appears at he beginning of the documentation to credit the
1224 | * authors, so it does not need to be generic and handle any situation.
1225 | *
1226 | * @see https://tdg.docbook.org/tdg/5.2/surname.html
1227 | */
1228 | protected function renderSurname(Node $node): Slotable|string
1229 | {
1230 | return $this->render->inlineText();
1231 | }
1232 |
1233 | /**
1234 | * A general-purpose element for representing the syntax of commands or functions.
1235 | *
1236 | * @see https://tdg.docbook.org/tdg/5.2/synopsis.html
1237 | */
1238 | protected function renderSynopsis(Node $node): Slotable|string
1239 | {
1240 | // Maybe not this?
1241 | return $this->render->component('program-listing');
1242 | }
1243 |
1244 | /**
1245 | * A system-related item or term.
1246 | *
1247 | * @see https://tdg.docbook.org/tdg/5.2/systemitem.html
1248 | */
1249 | protected function renderSystemItem(Node $node): Slotable|string
1250 | {
1251 | return $this->render->component('emphasised-literal');
1252 | }
1253 |
1254 | /**
1255 | * A formal (captioned) HTML table in a document.
1256 | *
1257 | * @see https://tdg.docbook.org/tdg/5.2/html.table.html
1258 | */
1259 | protected function renderTable(Node $node): Slotable|string
1260 | {
1261 | return $this->render->tag(
1262 | as: 'table',
1263 | class: 'my-6 w-full border-t border-l border-r border-violet-50',
1264 | );
1265 | }
1266 |
1267 | /**
1268 | * A wrapper for the rows of an HTML table or informal HTML table.
1269 | *
1270 | * @see https://tdg.docbook.org/tdg/5.2/html.tbody.html
1271 | */
1272 | protected function renderTBody(Node $node): Slotable|string
1273 | {
1274 | return $this->render->tag('tbody');
1275 | }
1276 |
1277 | /**
1278 | * The word or phrase being defined or described in a variable list.
1279 | *
1280 | * @see https://tdg.docbook.org/tdg/5.2/term.html
1281 | */
1282 | protected function renderTerm(Node $node): Slotable|string
1283 | {
1284 | $parent = $node->expectParent('varlistentry');
1285 |
1286 | return $this->render->tag(
1287 | as: 'dt',
1288 | class: 'space-x-2',
1289 | attributes: [
1290 | 'id' => $parent->hasId() ? $parent->id() : false,
1291 | ],
1292 | );
1293 | }
1294 |
1295 | /**
1296 | * A wrapper for the main content of a table, or part of a table.
1297 | *
1298 | * @see https://tdg.docbook.org/tdg/5.2/tgroup.html
1299 | */
1300 | protected function renderTGroup(Node $node): Slotable|string
1301 | {
1302 | return '';
1303 | }
1304 |
1305 | /**
1306 | * A table header consisting of one or more rows in an HTML table.
1307 | *
1308 | * @see https://tdg.docbook.org/tdg/5.2/html.thead.html
1309 | */
1310 | protected function renderTHead(Node $node): Slotable|string
1311 | {
1312 | return $this->render->tag(
1313 | as: 'thead',
1314 | class: 'bg-violet-50 text-violet-950 font-bold border-b border-violet-100',
1315 | );
1316 | }
1317 |
1318 | /**
1319 | * A suggestion to the user, set off from the text.
1320 | *
1321 | * @see https://tdg.docbook.org/tdg/5.2/tip.html
1322 | */
1323 | protected function renderTip(Node $node): Slotable|string
1324 | {
1325 | return $this->render->component('tip');
1326 | }
1327 |
1328 | /**
1329 | * The text of the title of a section of a document or of a formal block-level element.
1330 | *
1331 | * @see https://tdg.docbook.org/tdg/5.2/title.html
1332 | */
1333 | protected function renderTitle(Node $node): Slotable|string
1334 | {
1335 | // The headings in the preface are a special case. We will just force
1336 | // them to be level two without trying to do anything special.
1337 | if ($node->ancestor('section')?->hasAncestor('preface')) {
1338 | return $this->render->component('title', [
1339 | 'level' => 2,
1340 | 'link' => Link::fragment($node->hasId() ? $node->id() : $node->innerContent()),
1341 | ]);
1342 | }
1343 |
1344 | // Currently we hard code all note titles `h3`. I would love this to be
1345 | // improved.
1346 | if ($node->hasAncestor('note')) {
1347 | return $this->render->component('note.title', [
1348 | 'link' => Link::fragment($node->innerContent()),
1349 | ]);
1350 | }
1351 |
1352 | if ($node->hasAncestor('tip')) {
1353 | return $this->render->component('tip.title', [
1354 | 'link' => Link::fragment($node->innerContent()),
1355 | ]);
1356 | }
1357 |
1358 | // Currently we hard code all example titles `h2`. I would love this to
1359 | // be improved.
1360 | if ($node->hasParent('info.example')) {
1361 | return $this->render->component('title', [
1362 | 'level' => 2,
1363 | 'link' => Link::fragment($node->innerContent()),
1364 | ]);
1365 | }
1366 |
1367 | /*
1368 | * Now we will check if the node is contained within the title index.
1369 | * If it is, we assume it is a page title, as the index only contains
1370 | * the main page chunked titles.
1371 | */
1372 |
1373 | [$section] = $this->titleIndex->info($node);
1374 |
1375 | if ($section !== null) {
1376 | return $this->render->component('title', [
1377 | 'level' => 1,
1378 | 'link' => Link::fragment($section->id()),
1379 | ]);
1380 | }
1381 |
1382 | /**
1383 | * @todo this needs to caclculate the title depth to better set the "level"
1384 | */
1385 |
1386 | return $this->render->component('title', [
1387 | 'level' => 2,
1388 | 'link' => Link::fragment($node->innerContent()),
1389 | ]);
1390 | }
1391 |
1392 | /**
1393 | * The abbreviation of a title.
1394 | *
1395 | * @see https://tdg.docbook.org/tdg/5.2/titleabbrev.html
1396 | */
1397 | protected function renderTitleAbbrev(Node $node): Slotable|string
1398 | {
1399 | // TODO: would be cool if we could return a "skip" instruction to just skip over the entire node and its inner content.
1400 | // This is only used for the menu.
1401 | return '';
1402 | }
1403 |
1404 | /**
1405 | * Data entered by the user.
1406 | *
1407 | * @see https://tdg.docbook.org/tdg/5.2/userinput.html
1408 | */
1409 | protected function renderUserInput(Node $node): Slotable|string
1410 | {
1411 | return $this->render->component('inline-code');
1412 | }
1413 |
1414 | /**
1415 | * The classification of a value.
1416 | *
1417 | * @see https://tdg.docbook.org/tdg/5.2/type.html
1418 | */
1419 | protected function renderType(Node $node): Slotable|string
1420 | {
1421 | $link = $this->render->component('link', [
1422 | 'link' => Link::internal(match (strtolower($node->innerContent())) {
1423 | 'enum' => 'language.types.enumerations',
1424 | 'int' => 'language.types.integer',
1425 | 'bool' => 'language.types.boolean',
1426 | 'string' => 'language.types.string',
1427 | 'mixed' => 'language.types.mixed',
1428 | 'null' => 'language.types.null',
1429 | 'float' => 'language.types.float',
1430 | 'array' => 'language.types.array',
1431 | 'object' => 'language.types.object',
1432 | 'callable' => 'language.types.callable',
1433 | 'resource' => 'language.types.resource',
1434 | 'never' => 'language.types.never',
1435 | 'void' => 'language.types.void',
1436 | 'self', 'parent', 'static' => 'language.types.relative-class-types',
1437 | 'false' => 'reserved.constants#constant.false',
1438 | 'true' => 'reserved.constants#constant.true',
1439 | 'iterable' => 'language.types.iterable',
1440 | default => throw new RuntimeException('Unknown type encountered'),
1441 | }),
1442 | ]);
1443 |
1444 | return $this->render->component('inline-code')->wrapSlot($link);
1445 | }
1446 |
1447 | /**
1448 | * A list in which each entry is composed of a set of one or more terms and an associated description.
1449 | *
1450 | * @see https://tdg.docbook.org/tdg/5.2/variablelist.html
1451 | */
1452 | protected function renderVariableList(Node $node): Slotable|string
1453 | {
1454 | return $this->render->tag(
1455 | as: 'dl',
1456 | class: 'space-y-6',
1457 | );
1458 | }
1459 |
1460 | /**
1461 | * A wrapper for a set of terms and the associated description in a variable list.
1462 | *
1463 | * @see https://tdg.docbook.org/tdg/5.2/varlistentry.html
1464 | */
1465 | protected function renderVarListEntry(Node $node): Slotable|string
1466 | {
1467 | return $this->render->tag('div');
1468 | }
1469 |
1470 | /**
1471 | * The name of a variable.
1472 | *
1473 | * @see https://tdg.docbook.org/tdg/5.2/varname.html
1474 | *
1475 | * @todo link to the variable within the documentation. This will likely
1476 | * require further indexing. We can use `wrapSlot` to wrap the slot
1477 | * in a link tag.
1478 | */
1479 | protected function renderVarName(Node $node): Slotable|string
1480 | {
1481 | return $this->render->component('inline-code', [
1482 | 'as' => 'var',
1483 | ]);
1484 | // ->wrapSlot($this->render->component('link', [
1485 | // 'link' => Link::internal('#todo'),
1486 | // ]));
1487 | }
1488 |
1489 | /**
1490 | * An admonition set off from the text.
1491 | *
1492 | * @see https://tdg.docbook.org/tdg/5.2/warning.html
1493 | */
1494 | protected function renderWarning(Node $node): Slotable|string
1495 | {
1496 | return $this->render->component('warning');
1497 | }
1498 |
1499 | /**
1500 | * A cross reference to another part of the document.
1501 | *
1502 | * @see https://tdg.docbook.org/tdg/5.2/xref.html
1503 | */
1504 | protected function renderXref(Node $node): Slotable|string
1505 | {
1506 | return $this->render->component('link', [
1507 | 'link' => $node->link(),
1508 | 'text' => $node->link()->destination,
1509 | ]);
1510 | }
1511 |
1512 | /**
1513 | * The year of publication of a document.
1514 | *
1515 | * @see https://tdg.docbook.org/tdg/5.2/year.html
1516 | * @see self::copyright()
1517 | */
1518 | protected function renderYear(Node $node): Slotable|string
1519 | {
1520 | return '';
1521 | }
1522 |
1523 | /**
1524 | * Bail on unhandled node.
1525 | */
1526 | protected function unhandledNode(Node $node, string $reason): never
1527 | {
1528 | throw new RuntimeException("Unhandled node of tag [{$node->name}]. Reason: {$reason}.");
1529 | }
1530 |
1531 | /**
1532 | * Add debugging information to the node.
1533 | */
1534 | protected function withDebuggingInfo(Node $node, string|Slotable $content): string|Slotable
1535 | {
1536 | if (in_array($node->name, ['#text', '#cdata-section'], true)) {
1537 | return $content;
1538 | }
1539 |
1540 | [$name, $id, $role] = [
1541 | $node->name,
1542 | $node->hasId() ? $node->id() : '',
1543 | $node->hasRole() ? $node->role() : '',
1544 | ];
1545 |
1546 | if (is_string($content)) {
1547 | return <<
1549 | HTML.$content;
1550 | }
1551 |
1552 | return $this->render->wrapper(
1553 | before: <<
1555 | HTML.$content->before(),
1556 | after: $content->after(),
1557 | );
1558 | }
1559 | }
1560 |
--------------------------------------------------------------------------------
/src/Website/Image.php:
--------------------------------------------------------------------------------
1 |
35 | */
36 | protected ?Collection $allCache;
37 |
38 | /**
39 | * Create a new instance.
40 | */
41 | public function __construct(
42 | protected FileStreamFactory $streamFactory,
43 | protected Factory $render,
44 | protected Configuration $config,
45 | ) {
46 | $this->stream = $this->streamFactory->make(
47 | "{$this->config->get('index_directory')}/website/{$this->config->get('language')}/images.php",
48 | 1000,
49 | );
50 | }
51 |
52 | /**
53 | * Set up.
54 | */
55 | public function setUp(): void
56 | {
57 | $this->stream->write(<<<'PHP'
58 | stream;
76 | }
77 |
78 | /**
79 | * Determine if the generator should chunk.
80 | */
81 | public function shouldChunk(Node $node): bool
82 | {
83 | return false;
84 | }
85 |
86 | /**
87 | * Render the given node.
88 | */
89 | public function render(Node $node): string|Slotable
90 | {
91 | $mediaObject = $node->name === 'mediaobject'
92 | ? $node
93 | : $node->ancestor('mediaobject');
94 |
95 | if ($mediaObject === null) {
96 | return '';
97 | }
98 |
99 | return match ($node->name) {
100 | 'mediaobject' => $this->renderImageObject($node),
101 | 'imagedata' => $this->renderImageData($node),
102 | 'caption' => $this->renderCaption($node),
103 | 'alt' => $this->renderAlt($node),
104 | 'link' => $this->renderLink($node),
105 | '#text' => $this->renderText($node),
106 | 'simpara',
107 | 'imageobject' => '',
108 | default => throw new RuntimeException(<<name}] tag found in imageobject.
110 | Update the ImageIndex::render method.
111 |
112 | Content:
113 | {$node->innerContent()}
114 | ERROR),
115 | };
116 | }
117 |
118 | /**
119 | * Tear down.
120 | */
121 | public function tearDown(): void
122 | {
123 | $this->stream->write('];');
124 | }
125 |
126 | /**
127 | * Render an imageobject node.
128 | */
129 | protected function renderImageObject(Node $node): Slotable
130 | {
131 | $this->fileNumber = 1;
132 |
133 | return $this->render->wrapper(
134 | before: <<<'PHP'
135 | new Image(
136 | PHP,
137 | after: <<<'PHP'
138 |
139 | ),
140 |
141 |
142 | PHP,
143 | );
144 | }
145 |
146 | /**
147 | * Render an caption node.
148 | */
149 | protected function renderCaption(Node $node): Slotable
150 | {
151 | return $this->render->wrapper(
152 | before: "\n caption: new HtmlString(<<<'HTML'\n",
153 | after: "\nHTML),",
154 | );
155 | }
156 |
157 | /**
158 | * Render an imagedata node.
159 | */
160 | protected function renderImageData(Node $node): string
161 | {
162 | return "\n file".($this->fileNumber++).": {$this->render->export($node->attribute('fileref'))},";
163 | }
164 |
165 | /**
166 | * Render an alt node.
167 | */
168 | protected function renderLink(Node $node): Slotable
169 | {
170 | return $this->render->wrapper(
171 | before: <<
173 | HTML,
174 | after: <<<'HTML'
175 |
176 | HTML,
177 | );
178 | }
179 |
180 | /**
181 | * Render an alt node.
182 | */
183 | protected function renderAlt(Node $node): Slotable
184 | {
185 | return $this->render->wrapper(
186 | before: "\n alt: new HtmlString('",
187 | after: "'),",
188 | );
189 | }
190 |
191 | /**
192 | * Render a text node.
193 | */
194 | protected function renderText(Node $node): string
195 | {
196 | return e(Str::squish($node->value));
197 | }
198 |
199 | /**
200 | * Retrieve all titles from the index.
201 | *
202 | * @return Collection
203 | */
204 | public function all(): Collection
205 | {
206 | return $this->allCache ??= collect(require $this->stream->path); // @phpstan-ignore argument.templateType, argument.templateType
207 | }
208 |
209 | /**
210 | * Find the title based on it's ID.
211 | */
212 | public function find(string $id): Title
213 | {
214 | $title = $this->findMany(collect([$id]))->first();
215 |
216 | if ($title === null) {
217 | throw new RuntimeException("Could not find title with id [{$id}].");
218 | }
219 |
220 | return $title;
221 | }
222 |
223 | /**
224 | * Find many titles based on their ID.
225 | *
226 | * @param Collection $ids
227 | * @return Collection
228 | */
229 | public function findMany(Collection $ids): Collection
230 | {
231 | return $this->all()->only($ids);
232 | }
233 |
234 | /**
235 | * Retrieve the title heirachy.
236 | *
237 | * @return Collection
238 | */
239 | public function heirachy(): Collection
240 | {
241 | return $this->all()
242 | ->reduce(function (Collection $result, Title $title) {
243 | $level = 1;
244 | $children = $result;
245 |
246 | while ($level < $title->level) {
247 | $children = $children->reverse()->first()->children;
248 |
249 | $level++;
250 | }
251 |
252 | $children->push(new Title(
253 | id: $title->id,
254 | lineage: $title->lineage,
255 | level: $level,
256 | html: $result->isEmpty() ? new HtmlString('Home') : $title->html,
257 | ));
258 |
259 | return $result;
260 | }, collect([]));
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/src/Website/Method.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | public Collection $parameters;
17 |
18 | /**
19 | * The method return types.
20 | *
21 | * @var Collection
22 | */
23 | public Collection $returnTypes;
24 |
25 | /**
26 | * The method description.
27 | */
28 | public string $description;
29 |
30 | /**
31 | * Create a new instance.
32 | *
33 | * @param list $returnTypes
34 | */
35 | public function __construct(
36 | ?string $description,
37 | public string $name,
38 | array $returnTypes = [],
39 | ?Parameter $p1 = null,
40 | ?Parameter $p2 = null,
41 | ?Parameter $p3 = null,
42 | ?Parameter $p4 = null,
43 | ?Parameter $p5 = null,
44 | ?Parameter $p6 = null,
45 | ?Parameter $p7 = null,
46 | ?Parameter $p8 = null,
47 | ?Parameter $p9 = null,
48 | ?Parameter $p10 = null,
49 | ?Parameter $p11 = null,
50 | ?Parameter $p12 = null,
51 | ?Parameter $p13 = null,
52 | ?Parameter $p14 = null,
53 | ?Parameter $p15 = null,
54 | ?Parameter $p16 = null,
55 | ?Parameter $p17 = null,
56 | ?Parameter $p18 = null,
57 | ?Parameter $p19 = null,
58 | ?Parameter $p20 = null,
59 | ) {
60 | $this->description = $description ?? '';
61 |
62 | $this->returnTypes = collect($returnTypes);
63 |
64 | $this->parameters ??= collect([ // @phpstan-ignore assign.propertyType
65 | $p1, $p2, $p3, $p4, $p5,
66 | $p6, $p7, $p8, $p9, $p10,
67 | $p11, $p12, $p13, $p14, $p15,
68 | $p16, $p17, $p18, $p19, $p20,
69 | ])->filter()->values();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Website/Parameter.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | public Collection $types;
17 |
18 | /**
19 | * Create a new instance.
20 | *
21 | * @param list $types
22 | */
23 | public function __construct(
24 | array $types,
25 | public string $name,
26 | ) {
27 | $this->types = collect($types);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Website/Title.php:
--------------------------------------------------------------------------------
1 | $children
16 | */
17 | public function __construct(
18 | public string $id,
19 | public string $lineage,
20 | public int $level,
21 | public HtmlString $html,
22 | public Collection $children = new Collection,
23 | ) {
24 | //
25 | }
26 |
27 | /**
28 | * Determine if the title is the same or has the given title as a child.
29 | */
30 | public function isOrHasChild(Title $title): bool
31 | {
32 | return $this->is($title) || $this->hasChild($title);
33 | }
34 |
35 | /**
36 | * Determine if the title is the same as the given title.
37 | */
38 | public function is(Title $title): bool
39 | {
40 | return $this->id === $title->id;
41 | }
42 |
43 | /**
44 | * Determine if the title has the given title as a child.
45 | */
46 | public function hasChild(Title $title): bool
47 | {
48 | return $this->children->contains(
49 | fn (Title $child) => $child->is($title) || $child->hasChild($title),
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Website/TitleIndex.php:
--------------------------------------------------------------------------------
1 |
35 | */
36 | protected ?Collection $allCache;
37 |
38 | /**
39 | * Create a new instance.
40 | */
41 | public function __construct(
42 | protected FileStreamFactory $streamFactory,
43 | protected Factory $render,
44 | protected Configuration $config,
45 | ) {
46 | $this->stream = $this->streamFactory->make(
47 | "{$this->config->get('index_directory')}/website/{$this->config->get('language')}/titles.php",
48 | 1000,
49 | );
50 | }
51 |
52 | /**
53 | * Set up.
54 | */
55 | public function setUp(): void
56 | {
57 | $this->stream->write(<<<'PHP'
58 | stream;
76 | }
77 |
78 | /**
79 | * Determine if the generator should chunk.
80 | */
81 | public function shouldChunk(Node $node): bool
82 | {
83 | return false;
84 | }
85 |
86 | /**
87 | * Render the given node.
88 | */
89 | public function render(Node $node): string|Slotable
90 | {
91 | // We only care about nodes that are title tags or that are the
92 | // children of title tags.
93 | $title = $node->name === 'title'
94 | ? $node
95 | : $node->ancestor('title');
96 |
97 | if ($title === null) {
98 | return '';
99 | }
100 |
101 | // We do not want to capture the stray "PHP Manual" title.
102 | // if ($title->parent('book')?->hasId('manual')) {
103 | // return '';
104 | // }
105 |
106 | [$section, $level] = $this->info($title);
107 |
108 | if ($section === null) {
109 | return '';
110 | }
111 |
112 | return match ($node->name) {
113 | 'title' => $this->renderTitle($node, $section, $level),
114 | '#text' => $this->renderText($node),
115 | 'productname' => '',
116 | 'literal', 'command', 'function' => $this->render->tag('code'),
117 | 'classname' => $this->render->tag(
118 | as: 'var',
119 | class: 'not-italic',
120 | ),
121 | default => throw new RuntimeException(<<name}] tag found in title.
123 | Update the TitleIndex::render method.
124 |
125 | Content:
126 | {$node->innerContent()}
127 | ERROR),
128 | };
129 | }
130 |
131 | /**
132 | * Tear down.
133 | */
134 | public function tearDown(): void
135 | {
136 | $this->stream->write('];');
137 | }
138 |
139 | /**
140 | * Render a title node.
141 | */
142 | protected function renderTitle(Node $title, Node $section, int $level): Slotable
143 | {
144 | return $this->render->wrapper(
145 | before: <<render->export($section->id())} => new Title(
147 | id: {$this->render->export($section->id())},
148 | lineage: '{$title->lineage()}',
149 | level: {$level},
150 | html: new HtmlString(<<<'HTML'
151 |
152 | PHP,
153 | after: <<<'PHP'
154 |
155 | HTML)),
156 |
157 |
158 | PHP,
159 | );
160 | }
161 |
162 | /**
163 | * Render a text node.
164 | */
165 | protected function renderText(Node $node): string
166 | {
167 | return e(Str::squish($node->value));
168 | }
169 |
170 | /**
171 | * Retrieve the node's title information.
172 | *
173 | * @return array{ 0: Node|null, 1: int }
174 | */
175 | public function info(Node $title): array
176 | {
177 | // Once we hit the function reference section the rules change, so we
178 | // will check if we are in the function reference or not.
179 | $isFunctionRef = (bool) $title->parent('set')?->hasId('funcref');
180 |
181 | // When in the function reference, we modify each title within the
182 | // function reference to be one level deeper.
183 | if ($isFunctionRef) {
184 | $this->levelModifier = 1;
185 | }
186 |
187 | // The FAQs should then revert and use no level modifier.
188 | if ($title->parent('book')?->hasId('faq')) {
189 | $this->levelModifier = 0;
190 | }
191 |
192 | return match (true) {
193 | $isFunctionRef => [$title->parent('set'), 1],
194 | $title->hasParent('book.set') => [$title->parent('book'), 1 + $this->levelModifier],
195 | $title->hasParent('chapter.book.set.set.set') => [$title->parent('chapter'), 2 + $this->levelModifier],
196 | $title->hasParent('chapter.book.set') => [$title->parent('chapter'), 2 + $this->levelModifier],
197 | $title->hasParent('info.chapter.book.set') => [$title->parent('info.chapter'), 2 + $this->levelModifier],
198 | $title->hasParent('info.legalnotice.info.set') => [$title->parent('info.legalnotice'), 1 + $this->levelModifier],
199 | $title->hasParent('info.preface.book.set') => [$title->parent('info.preface'), 1 + $this->levelModifier],
200 | $title->hasParent('info.section.chapter.book.set') => [$title->parent('info.section'), 3 + $this->levelModifier],
201 | $title->hasParent('preface.book.set.set.set') => [$title->parent('preface'), 2 + $this->levelModifier],
202 | $title->hasParent('sect1.chapter.book.set') => [$title->parent('sect1'), 3 + $this->levelModifier],
203 | $title->hasParent('section.chapter.book.set.set.set') && $title->expectParent('section')->hasId() => [$title->parent('section'), 3 + $this->levelModifier],
204 | $title->hasParent('set') => [$title->parent('set'), 1 + $this->levelModifier],
205 | default => [null, 0],
206 | };
207 | }
208 |
209 | /**
210 | * Retrieve all titles from the index.
211 | *
212 | * @return Collection
213 | */
214 | public function all(): Collection
215 | {
216 | return $this->allCache ??= collect(require $this->stream->path); // @phpstan-ignore argument.templateType, argument.templateType
217 | }
218 |
219 | /**
220 | * Find the title based on it's ID.
221 | */
222 | public function find(string $id): Title
223 | {
224 | $title = $this->findMany(collect([$id]))->first();
225 |
226 | if ($title === null) {
227 | throw new RuntimeException("Could not find title with id [{$id}].");
228 | }
229 |
230 | return $title;
231 | }
232 |
233 | /**
234 | * Find many titles based on their ID.
235 | *
236 | * @param Collection $ids
237 | * @return Collection
238 | */
239 | public function findMany(Collection $ids): Collection
240 | {
241 | return $this->all()->only($ids);
242 | }
243 |
244 | /**
245 | * Retrieve the title heirachy.
246 | *
247 | * @return Collection
248 | */
249 | public function heirachy(): Collection
250 | {
251 | return $this->all()
252 | ->reduce(function (Collection $result, Title $title) {
253 | $level = 1;
254 | $children = $result;
255 |
256 | while ($level < $title->level) {
257 | $children = $children->reverse()->first()->children;
258 |
259 | $level++;
260 | }
261 |
262 | $children->push(new Title(
263 | id: $title->id,
264 | lineage: $title->lineage,
265 | level: $level,
266 | html: $result->isEmpty() ? new HtmlString('Home') : $title->html,
267 | ));
268 |
269 | return $result;
270 | }, collect([]));
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import defaultTheme from 'tailwindcss/defaultTheme'
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | content: [
6 | "./build/output/*.html",
7 | ],
8 | darkMode: 'selector',
9 | theme: {
10 | extend: {
11 | fontFamily: {
12 | mono: ['"Roboto Mono"', ...defaultTheme.fontFamily.mono],
13 | },
14 | colors: {
15 | violet: {
16 | '25': '#f8f5ff',
17 | }
18 | }
19 | },
20 | },
21 | plugins: [],
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/theme.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "light",
3 | "colors": {
4 | "activityBar.activeBorder": "#6d6d6d",
5 | "activityBar.background": "#24292e",
6 | "activityBar.foreground": "#fafbfc",
7 | "activityBarBadge.background": "#d64926",
8 | "button.background": "#5790be",
9 | "editor.background": "#f9f9f9",
10 | "editor.foreground": "#24292e",
11 | "editor.lineHighlightBackground": "#eaeff5",
12 | "editorBracketMatch.background": "#f1f8ff",
13 | "editorBracketMatch.border": "#c8e1ff",
14 | "editorGroup.border": "#cecece",
15 | "editorGroupHeader.tabsBackground": "#fafbfc",
16 | "editorGroupHeader.tabsBorder": "#e1e4e8",
17 | "editorIndentGuide.background": "#eeeeee",
18 | "editorLineNumber.foreground": "#cccccc",
19 | "focusBorder": "#4b4b4b",
20 | "input.border": "#d1d1d1",
21 | "list.activeSelectionBackground": "#5790be",
22 | "list.activeSelectionForeground": "#f0f0f0",
23 | "list.activeSelectionIconForeground": "#f0f0f0",
24 | "list.focusHighlightForeground": "#ffffff",
25 | "list.inactiveSelectionBackground": "#d0e2f0",
26 | "menu.background": "#24292e",
27 | "menu.border": "#494949",
28 | "menu.foreground": "#cccccc",
29 | "menu.selectionBackground": "#9e9e9e",
30 | "menu.separatorBackground": "#494949",
31 | "menubar.selectionBackground": "#494949",
32 | "panel.background": "#f3f3f3",
33 | "scrollbar.shadow": "#cccccc",
34 | "sideBar.background": "#f3f3f3",
35 | "sideBar.border": "#cecece",
36 | "sideBar.foreground": "#586069",
37 | "sideBarSectionHeader.background": "#e6e6e6",
38 | "sideBarSectionHeader.foreground": "#616161",
39 | "sideBarTitle.foreground": "#24292e",
40 | "statusBar.background": "#24292e",
41 | "statusBar.border": "#ff000000",
42 | "statusBar.debuggingBackground": "#d64926",
43 | "statusBar.debuggingBorder": "#ff000000",
44 | "statusBar.debuggingForeground": "#cccccc",
45 | "statusBar.foreground": "#cccccc",
46 | "statusBar.noFolderBackground": "#24292e",
47 | "statusBar.noFolderBorder": "#ff000000",
48 | "statusBar.noFolderForeground": "#cccccc",
49 | "statusBarItem.remoteBackground": "#d64926",
50 | "statusBarItem.remoteForeground": "#ffffff",
51 | "tab.activeBackground": "#ffffff",
52 | "tab.activeBorder": "#e36209",
53 | "tab.border": "#e1e4e8",
54 | "tab.inactiveBackground": "#fafbfc",
55 | "tab.inactiveForeground": "#586069",
56 | "terminal.background": "#f3f3f3",
57 | "terminal.tab.activeBorder": "#00000000",
58 | "titleBar.activeBackground": "#24292e",
59 | "titleBar.activeForeground": "#cccccc",
60 | "titleBar.border": "#ff000000",
61 | "titleBar.inactiveBackground": "#24292e"
62 | },
63 | "tokenColors": [
64 | {
65 | "settings": {
66 | "background": "transparent",
67 | "foreground": "#24292eff"
68 | }
69 | },
70 | {
71 | "scope": [
72 | "keyword.operator.accessor",
73 | "meta.group.braces.round.function.arguments",
74 | "meta.template.expression",
75 | "markup.fenced_code meta.embedded.block"
76 | ],
77 | "settings": {
78 | "foreground": "#24292eff"
79 | }
80 | },
81 | {
82 | "scope": "emphasis",
83 | "settings": {
84 | "fontStyle": "italic"
85 | }
86 | },
87 | {
88 | "scope": [
89 | "strong",
90 | "markup.heading.markdown",
91 | "markup.bold.markdown"
92 | ],
93 | "settings": {
94 | "fontStyle": "bold"
95 | }
96 | },
97 | {
98 | "scope": [
99 | "markup.italic.markdown"
100 | ],
101 | "settings": {
102 | "fontStyle": "italic"
103 | }
104 | },
105 | {
106 | "scope": "meta.link.inline.markdown",
107 | "settings": {
108 | "fontStyle": "underline",
109 | "foreground": "#005cc5"
110 | }
111 | },
112 | {
113 | "scope": [
114 | "comment",
115 | "markup.fenced_code",
116 | "markup.inline"
117 | ],
118 | "settings": {
119 | "foreground": "#6a737d"
120 | }
121 | },
122 | {
123 | "scope": "string",
124 | "settings": {
125 | "foreground": "#032f62"
126 | }
127 | },
128 | {
129 | "scope": [
130 | "constant.numeric",
131 | "constant.language",
132 | "variable.language.this",
133 | "variable.other.class",
134 | "variable.other.constant",
135 | "meta.property-name",
136 | "meta.property-value",
137 | "support"
138 | ],
139 | "settings": {
140 | "foreground": "#005cc5"
141 | }
142 | },
143 | {
144 | "scope": [
145 | "keyword",
146 | "storage.modifier",
147 | "storage.type",
148 | "storage.control.clojure",
149 | "entity.name.function.clojure",
150 | "support.function.node",
151 | "support.type.property-name.json",
152 | "punctuation.separator.key-value",
153 | "punctuation.definition.template-expression"
154 | ],
155 | "settings": {
156 | "foreground": "#d73a49"
157 | }
158 | },
159 | {
160 | "scope": "variable.parameter.function",
161 | "settings": {
162 | "foreground": "#E27F2D"
163 | }
164 | },
165 | {
166 | "scope": [
167 | "entity.name.type",
168 | "entity.other.inherited-class",
169 | "meta.function-call",
170 | "meta.instance.constructor",
171 | "entity.other.attribute-name",
172 | "entity.name.function",
173 | "constant.keyword.clojure"
174 | ],
175 | "settings": {
176 | "foreground": "#6f42c1"
177 | }
178 | },
179 | {
180 | "scope": [
181 | "entity.name.tag",
182 | "string.quoted",
183 | "string.regexp",
184 | "string.interpolated",
185 | "string.template",
186 | "keyword.other.template"
187 | ],
188 | "settings": {
189 | "foreground": "#22863a"
190 | }
191 | },
192 | {
193 | "scope": "token.info-token",
194 | "settings": {
195 | "foreground": "#316bcd"
196 | }
197 | },
198 | {
199 | "scope": "token.warn-token",
200 | "settings": {
201 | "foreground": "#cd9731"
202 | }
203 | },
204 | {
205 | "scope": "token.error-token",
206 | "settings": {
207 | "foreground": "#cd3131"
208 | }
209 | },
210 | {
211 | "scope": "token.debug-token",
212 | "settings": {
213 | "foreground": "#800080"
214 | }
215 | }
216 | ]
217 | }
218 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('vite').UserConfig} */
2 | export default {
3 | build: {
4 | outDir: "build/output",
5 | emptyOutDir: false,
6 | rollupOptions: {
7 | input: ["resources/script.js", "resources/style.css"],
8 | output: {
9 | assetFileNames: "[name][extname]",
10 | entryFileNames: "[name].js",
11 | },
12 | },
13 | },
14 | }
15 |
--------------------------------------------------------------------------------