packages/sql-catalog/src/Reporter/Html/HtmlReporter.php

1<?php
2
3declare(strict_types=1);
4
5namespace SqlCatalog\Reporter\Html;
6
7use Override;
8use SqlCatalog\Core\Catalog\Catalog;
9use SqlCatalog\Core\Reporter\CatalogArtifacts;
10use SqlCatalog\Core\Reporter\ReporterInterface;
11use SqlCatalog\Reporter\Html\Page\ClassPage;
12use SqlCatalog\Reporter\Html\Page\FileIndexPage;
13use SqlCatalog\Reporter\Html\Page\FilePage;
14use SqlCatalog\Reporter\Html\Page\FindingPage;
15use SqlCatalog\Reporter\Html\Page\NamespacePage;
16use SqlCatalog\Reporter\Html\Page\OverviewPage;
17use SqlCatalog\Reporter\Html\Page\StatementIndexPage;
18use SqlCatalog\Reporter\Html\Page\StatementPage;
19use SqlCatalog\Reporter\Html\Page\TableIndexPage;
20use SqlCatalog\Reporter\Html\Page\TablePage;
21
22/**
23 * Writes the catalog as a site of linked HTML pages.
24 *
25 * A reader comes to a catalog to find a statement and decide something about
26 * it, so the site is laid out as the routes to one: by the table it names, by
27 * the namespace and class that issue it, by the file it is written in, by
28 * what the analysis reported on it, or through the whole listing narrowed
29 * down. Every statement has a page of its own, which is where every route
30 * ends.
31 *
32 * @visibility root
33 */
34final class HtmlReporter implements ReporterInterface
35{
36    /**
37     * Supplies SQL presentation independently of page rendering.
38     */
39    public function __construct(private readonly SqlFormatter $formatter = new SqlFormatter())
40    {
41    }
42
43    /**
44     * The name the page a reader opens first is written under.
45     */
46    public const FILE = ReportSite::INDEX;
47
48    /**
49     * The name the command line selects this reporter by.
50     */
51    #[Override]
52    public function name(): string
53    {
54        return 'html';
55    }
56
57    /**
58     * What the reporter produces.
59     */
60    #[Override]
61    public function description(): string
62    {
63        return 'a site of linked HTML pages, for reading and sharing';
64    }
65
66    /**
67     * The catalog rendered as a site of pages.
68     */
69    #[Override]
70    public function render(Catalog $catalog): CatalogArtifacts
71    {
72        $site = new ReportSite($catalog, formatter: $this->formatter);
73        $shell = new PageShell();
74        $home = ['Overview', ReportSite::INDEX];
75
76        $files = (new ReportAssets())->all();
77        $files[PageShell::INDEX] = (new SearchIndex())->render($site);
78        $files[ReportSite::INDEX] = $shell->render($site, ReportSite::INDEX, 'Overview', [['Overview', null]], (new OverviewPage())->render($site), $shell->onThisPage([
79            ['Needs attention', 'attention'],
80            ['How far the analysis got', 'coverage'],
81        ]));
82        $files[ReportSite::STATEMENTS] = $shell->render($site, ReportSite::STATEMENTS, 'Statements', [$home, ['Statements', null]], (new StatementIndexPage())->render($site));
83        $files[ReportSite::TABLES] = $shell->render($site, ReportSite::TABLES, 'Tables', [$home, ['Tables', null]], (new TableIndexPage())->render($site));
84        $namespaces = new NamespacePage();
85        $files[ReportSite::NAMESPACES] = $shell->render($site, ReportSite::NAMESPACES, 'Namespaces', [$home, ['Namespaces', null]], $namespaces->render($site), $shell->onThisPage($namespaces->anchors($site)));
86        $directories = new FileIndexPage();
87        $files[ReportSite::FILES] = $shell->render($site, ReportSite::FILES, 'Files', [$home, ['Files', null]], $directories->render($site), $shell->onThisPage($directories->anchors($site)));
88        $findings = new FindingPage();
89        $files[ReportSite::FINDINGS] = $shell->render($site, ReportSite::FINDINGS, 'Findings', [$home, ['Findings', null]], $findings->render($site), $shell->onThisPage($findings->anchors($site)));
90
91        return new CatalogArtifacts(
92            array_merge($files, $this->tablePages($site, $shell), $this->classPages($site, $shell), $this->filePages($site, $shell), $this->statementPages($site, $shell)),
93            ReportSite::INDEX,
94        );
95    }
96
97    /**
98     * One page per table the catalog names.
99     *
100     * @return array<string, string>
101     */
102    public function tablePages(ReportSite $site, PageShell $shell): array
103    {
104        $page = new TablePage();
105        $pages = [];
106        foreach ($site->tables() as $table) {
107            $name = $site->tablePage($table);
108            $label = (new TableName($table))->label();
109            $pages[$name] = $shell->render(
110                $site,
111                $name,
112                $label,
113                [['Overview', ReportSite::INDEX], ['Tables', ReportSite::TABLES], [$label, null]],
114                $page->render($site, $table),
115                $page->context($site, $table),
116            );
117        }
118
119        return $pages;
120    }
121
122    /**
123     * One page per class that issues a statement.
124     *
125     * @return array<string, string>
126     */
127    public function classPages(ReportSite $site, PageShell $shell): array
128    {
129        $page = new ClassPage();
130        $pages = [];
131        foreach ($site->classes() as $class) {
132            $name = $site->classPage($class);
133            $pages[$name] = $shell->render(
134                $site,
135                $name,
136                $class,
137                [['Overview', ReportSite::INDEX], ['Namespaces', ReportSite::NAMESPACES], [Scope::of($class . '::x')->classShort() ?? $class, null]],
138                $page->render($site, $class),
139                $page->context($site, $class),
140            );
141        }
142
143        return $pages;
144    }
145
146    /**
147     * One page per file a statement is written in.
148     *
149     * @return array<string, string>
150     */
151    public function filePages(ReportSite $site, PageShell $shell): array
152    {
153        $page = new FilePage();
154        $pages = [];
155        foreach ($site->files() as $file) {
156            $name = $site->filePage($file);
157            $pages[$name] = $shell->render(
158                $site,
159                $name,
160                $file,
161                [['Overview', ReportSite::INDEX], ['Files', ReportSite::FILES], [$file, null]],
162                $page->render($site, $file),
163                $page->context($site, $file),
164            );
165        }
166
167        return $pages;
168    }
169
170    /**
171     * One page per statement.
172     *
173     * @return array<string, string>
174     */
175    public function statementPages(ReportSite $site, PageShell $shell): array
176    {
177        $page = new StatementPage();
178        $pages = [];
179        foreach ($site->index()->entries() as $entry) {
180            $name = $site->statementPage($entry->id);
181            $pages[$name] = $shell->render(
182                $site,
183                $name,
184                strtoupper($entry->kind->value) . ' at ' . $entry->site->display(),
185                [['Overview', ReportSite::INDEX], ['Statements', ReportSite::STATEMENTS], [$entry->id, null]],
186                $page->render($site, $entry),
187                $page->context($site, $entry),
188            );
189        }
190
191        return $pages;
192    }
193}
194