Issues (57)

src/DumpDocs.php (3 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Koriym\AppStateDiagram;
6
7
use stdClass;
8
9
use function assert;
10
use function basename;
11
use function dirname;
12
use function explode;
13
use function file_put_contents;
14
use function filter_var;
15
use function implode;
16
use function is_dir;
17
use function is_string;
18
use function mkdir;
19
use function property_exists;
20
use function sprintf;
21
use function str_replace;
22
use function strpos;
23
use function substr;
24
use function usort;
25
26
use const FILTER_VALIDATE_URL;
27
use const PHP_EOL;
28
29
/** @psalm-suppress MissingConstructor */
30
final class DumpDocs
31
{
32
    public const MODE_HTML = 'html';
33
    public const MODE_MARKDOWN = 'markdown';
34
35
    /** @var array<string, AbstractDescriptor> */
36
    private $descriptors = [];
37
38
    /** @var "html"|"md" */
0 ignored issues
show
Documentation Bug introduced by
The doc comment "html"|"md" at position 0 could not be parsed: Unknown type name '"html"' at position 0 in "html"|"md".
Loading history...
39
    private $ext;
40
41
    public function __invoke(Profile $profile, string $alpsFile, string $format = self::MODE_HTML): void
42
    {
43
        $descriptors = $this->descriptors = $profile->descriptors;
44
        $this->ext = $format === self::MODE_MARKDOWN ? 'md' : self::MODE_HTML;
45
        $docsDir = $this->mkDir(dirname($alpsFile), 'docs');
46
        $asdFile = sprintf('../%s', basename(str_replace(['xml', 'json'], 'svg', $alpsFile)));
47
        foreach ($descriptors as $descriptor) {
48
            $markDown = $this->getSemanticDoc($descriptor, $asdFile, $profile->title);
49
            $basePath = sprintf('%s/%s.%s', $docsDir, $descriptor->type, $descriptor->id);
50
            $title = "{$descriptor->id} ({$descriptor->type})";
51
            $this->fileOutput($title, $markDown, $basePath, $format);
52
        }
53
54
        foreach ($profile->tags as $tag => $descriptorIds) {
55
            $markDown = $this->getTagDoc($tag, $descriptorIds, $profile->title, $asdFile);
56
            $basePath = sprintf('%s/tag.%s', $docsDir, $tag);
57
            $this->fileOutput($tag, $markDown, $basePath, $format);
58
        }
59
60
        $this->dumpImage($profile->title, $docsDir, $format, $alpsFile, '');
61
        $this->dumpImage($profile->title, $docsDir, $format, $alpsFile, 'title.');
62
    }
63
64
    private function dumpImage(string $title, string $docsDir, string $format, string $alpsFile, string $type): void
65
    {
66
        $imgSrc = str_replace(['json', 'xml'], "{$type}svg", basename($alpsFile));
67
        $format === self::MODE_HTML ?
68
            $this->dumpImageHtml($title, $docsDir, $imgSrc, $type) :
69
            $this->dumpImageMd($docsDir, $imgSrc, $type);
70
    }
71
72
    private function dumpImageMd(string $docsDir, string $imgSrc, string $type): void
73
    {
74
        $isIdMode = $type === '';
75
        $link = $isIdMode ? 'id | [title](asd.title.md)' : '[id](asd.md) | title';
76
        $html = <<<EOT
77
{$link}
78
79
[<img src="../{$imgSrc}" alt="application state diagram">](../{$imgSrc})
80
EOT;
81
        file_put_contents($docsDir . "/asd.{$type}md", $html);
82
    }
83
84
    private function dumpImageHtml(string $title, string $docsDir, string $imgSrc, string $type): void
85
    {
86
        $isIdMode = $type === '';
87
        $link = $isIdMode ? '<ul class="diagram-mode"> <li class="diagram-mode__item"> <span class="diagram-mode__text">id</span> </li><li class="diagram-mode__item"> <a href="asd.title.html" class="diagram-mode__link">title</a> </li></ul>' : '<ul class="diagram-mode"> <li class="diagram-mode__item"> <a href="asd.html" class="diagram-mode__link">id</a> </li><li class="diagram-mode__item"> <span class="diagram-mode__text">title</span> </li></ul>';
88
        $html = <<<EOT
89
<html lang="en">
90
<head>
91
    <title>{$title}</title>
92
    <meta charset="UTF-8">
93
    <style>
94
      :root {
95
        --color-text-base: #24292e;
96
        --color-border-base: #eaecef;
97
        --color-link-base: #3366cc;
98
        --font-size-base: 1rem;
99
      }
100
101
      body {
102
        font-family: sans-serif;
103
      }
104
105
      .diagram-mode {
106
        margin-block-end: 2rem;
107
        border-bottom: 1px solid var(--color-border-base);
108
        font-size: var(--font-size-base);
109
      }
110
111
      .diagram-mode__item {
112
        display: inline-flex;
113
        margin-inline: 0.5rem;
114
        list-style: none;
115
        color: var(--color-text-base);
116
      }
117
118
      .diagram-mode__text {
119
        display: inline-flex;
120
        position: relative;
121
        box-sizing: border-box;
122
        max-height: 4em;
123
        margin-block-end: -1px;
124
        padding-block: 1.5rem 0.5rem;
125
        border-bottom: 1px solid;
126
        text-decoration: none;
127
        color: var(--color-text-base);
128
      }
129
130
      .diagram-mode__link {
131
        display: inline-flex;
132
        position: relative;
133
        box-sizing: border-box;
134
        max-height: 4em;
135
        margin-block-end: -1px;
136
        padding-block: 1.5rem 0.5rem;
137
        cursor: pointer;
138
        color: var(--color-link-base);
139
        text-decoration: none;
140
      }
141
142
      .diagram-mode__link:hover,
143
      .diagram-mode__link:focus {
144
        border-bottom: 1px solid;
145
      }
146
    </style>
147
</head>
148
<body>
149
    <div>{$link}</div>
150
    <iframe src="../{$imgSrc}" style="border:0; width:100%; height:95%" allow="fullscreen"></iframe>
151
</body>
152
</html>
153
154
EOT;
155
        file_put_contents($docsDir . "/asd.{$type}html", $html);
156
    }
157
158
    private function convertHtml(string $title, string $markdown): string
159
    {
160
        return (new MdToHtml())($title, $markdown) . PHP_EOL;
161
    }
162
163
    private function fileOutput(string $title, string $markDown, string $basePath, string $format): void
164
    {
165
        $file = sprintf('%s.%s', $basePath, $this->ext);
166
        if ($format === self::MODE_MARKDOWN) {
167
            file_put_contents($file, $markDown);
168
169
            return;
170
        }
171
172
        file_put_contents($file, $this->convertHtml($title, $markDown));
173
    }
174
175
    private function mkDir(string $baseDir, string $dirName): string
176
    {
177
        $dir = sprintf('%s/%s', $baseDir, $dirName);
178
        if (! is_dir($dir)) {
179
            mkdir($dir, 0777, true); // @codeCoverageIgnore
180
        }
181
182
        return $dir;
183
    }
184
185
    private function getSemanticDoc(AbstractDescriptor $descriptor, string $asd, string $title): string
186
    {
187
        $descriptorSemantic = $this->getDescriptorInDescriptor($descriptor);
188
        $rt = $this->getRt($descriptor);
189
        $description = '';
190
        $description .= $this->getDescriptorProp('type', $descriptor);
191
        $description .= $this->getDescriptorProp('title', $descriptor);
192
        $description .= $this->getDescriptorProp('href', $descriptor);
193
        $description .= $this->getDescriptorKeyValue('doc', (string) ($descriptor->doc->value ?? ''));
194
        $description .= $this->getDescriptorProp('def', $descriptor);
195
        $description .= $this->getDescriptorProp('rel', $descriptor);
196
        $description .= $this->getTag($descriptor->tags);
197
        $linkRelations = $this->getLinkRelations($descriptor->linkRelations);
198
        $titleHeader = $title ? sprintf('%s: Semantic Descriptor', $title) : 'Semantic Descriptor';
199
200
        return <<<EOT
201
{$titleHeader}
202
# {$descriptor->id}
203
{$description}{$rt}{$linkRelations}{$descriptorSemantic}
204
---
205
206
[home](../index.{$this->ext}) | [asd]($asd)
207
EOT;
208
    }
209
210
    private function getDescriptorProp(string $key, AbstractDescriptor $descriptor): string
211
    {
212
        if (! property_exists($descriptor, $key) || ! $descriptor->{$key}) {
213
            return '';
214
        }
215
216
        $value = (string) $descriptor->{$key};
217
        if ($this->isUrl($value)) {
218
            return " * {$key}: [{$value}]({$value})" . PHP_EOL;
219
        }
220
221
        if ($this->isFragment($value)) {
222
            [, $id] = explode('#', $value);
223
224
            return " * {$key}: [{$id}](semantic.{$id}.{$this->ext})" . PHP_EOL;
225
        }
226
227
        return " * {$key}: {$value}" . PHP_EOL;
228
    }
229
230
    private function isUrl(string $text): bool
231
    {
232
        return filter_var($text, FILTER_VALIDATE_URL) !== false;
233
    }
234
235
    private function isFragment(string $text): bool
236
    {
237
        return $text[0] === '#';
238
    }
239
240
    private function getDescriptorKeyValue(string $key, string $value): string
241
    {
242
        if (! $value) {
243
            return '';
244
        }
245
246
        return " * {$key}: {$value}" . PHP_EOL;
247
    }
248
249
    private function getRt(AbstractDescriptor $descriptor): string
250
    {
251
        if ($descriptor instanceof SemanticDescriptor) {
252
            return '';
253
        }
254
255
        assert($descriptor instanceof TransDescriptor);
256
257
        return sprintf(' * rt: [%s](semantic.%s.%s)', $descriptor->rt, $descriptor->rt, $this->ext) . PHP_EOL;
258
    }
259
260
    private function getDescriptorInDescriptor(AbstractDescriptor $descriptor): string
261
    {
262
        if ($descriptor->descriptor === []) {
263
            return '';
264
        }
265
266
        $descriptors = $this->getInlineDescriptors($descriptor->descriptor);
267
268
        $table = sprintf(' * descriptor%s%s| id | type | title |%s|---|---|---|%s', PHP_EOL, PHP_EOL, PHP_EOL, PHP_EOL);
269
        foreach ($descriptors as $descriptor) {
270
            $table .= sprintf('| %s | %s | %s |', $descriptor->htmlLink($this->ext), $descriptor->type, $descriptor->title) . PHP_EOL;
271
        }
272
273
        return $table;
274
    }
275
276
    /**
277
     * @param non-empty-list<stdClass> $inlineDescriptors
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<stdClass> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<stdClass>.
Loading history...
278
     *
279
     * @return non-empty-list<AbstractDescriptor>
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<AbstractDescriptor> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<AbstractDescriptor>.
Loading history...
280
     */
281
    private function getInlineDescriptors(array $inlineDescriptors): array
282
    {
283
        $descriptors = [];
284
        foreach ($inlineDescriptors as $descriptor) {
285
            if (isset($descriptor->id)) {
286
                assert(is_string($descriptor->id));
287
                $descriptors[] = $this->descriptors[$descriptor->id];
288
                continue;
289
            }
290
291
            assert(is_string($descriptor->href));
292
            $id = substr($descriptor->href, (int) strpos($descriptor->href, '#') + 1);
293
            assert(isset($this->descriptors[$id]));
294
295
            $original = clone $this->descriptors[$id];
296
            if (isset($descriptor->title)) {
297
                $original->title = (string) $descriptor->title;
298
            }
299
300
            $descriptors[] = $original;
301
        }
302
303
        usort($descriptors, static function (AbstractDescriptor $a, AbstractDescriptor $b): int {
304
            $order = ['semantic' => 0, 'safe' => 1, 'unsafe' => 2, 'idempotent' => 3];
305
306
            return $order[$a->type] <=> $order[$b->type];
307
        });
308
309
        assert($descriptors !== []);
310
311
        return $descriptors;
312
    }
313
314
    /** @param list<string> $tags */
315
    private function getTag(array $tags): string
316
    {
317
        if ($tags === []) {
318
            return '';
319
        }
320
321
        return " * tag: {$this->getTagString($tags)}";
322
    }
323
324
    /** @param list<string> $tags */
325
    private function getTagString(array $tags): string
326
    {
327
        $string = [];
328
        foreach ($tags as $tag) {
329
            $string[] = "[{$tag}](tag.{$tag}.{$this->ext})";
330
        }
331
332
        return implode(', ', $string) . PHP_EOL;
333
    }
334
335
    /** @param list<string> $descriptorIds */
336
    private function getTagDoc(string $tag, array $descriptorIds, string $title, string $asd): string
337
    {
338
        $list = '';
339
        foreach ($descriptorIds as $descriptorId) {
340
            $descriptor = $this->descriptors[$descriptorId];
341
            $list .= " * {$descriptor->htmlLink($this->ext)}" . PHP_EOL;
342
        }
343
344
        $titleHeader = $title ? sprintf('%s: Tag', $title) : 'Tag';
345
346
        return <<<EOT
347
{$titleHeader}
348
# {$tag}
349
350
{$list}
351
---
352
353
[home](../index.{$this->ext}) | [asd]({$asd}) | {$tag} 
354
EOT;
355
    }
356
357
    private function getLinkRelations(LinkRelations $linkRelations): string
358
    {
359
        if ((string) $linkRelations === '') {
360
            return '';
361
        }
362
363
        return ' * links' . PHP_EOL . $linkRelations . PHP_EOL;
364
    }
365
}
366