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" */ |
|
|
|
|
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 |
|
|
|
|
278
|
|
|
* |
279
|
|
|
* @return non-empty-list<AbstractDescriptor> |
|
|
|
|
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
|
|
|
|