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 ? 'id | <a href="asd.title.html">title</a>' : '<a href="asd.html">id</a> | title'; |
88
|
|
|
$html = <<<EOT |
89
|
|
|
<html lang="en"> |
90
|
|
|
<head> |
91
|
|
|
<title>{$title}</title> |
92
|
|
|
<meta charset="UTF-8"> |
93
|
|
|
</head> |
94
|
|
|
<body> |
95
|
|
|
<div style="font-size: medium;" >{$link}</div> |
96
|
|
|
<iframe src="../{$imgSrc}" style="border:0; width:100%; height:95%" allow="fullscreen"></iframe> |
97
|
|
|
</body> |
98
|
|
|
</html> |
99
|
|
|
|
100
|
|
|
EOT; |
101
|
|
|
file_put_contents($docsDir . "/asd.{$type}html", $html); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
private function convertHtml(string $title, string $markdown): string |
105
|
|
|
{ |
106
|
|
|
return (new MdToHtml())($title, $markdown) . PHP_EOL; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
private function fileOutput(string $title, string $markDown, string $basePath, string $format): void |
110
|
|
|
{ |
111
|
|
|
$file = sprintf('%s.%s', $basePath, $this->ext); |
112
|
|
|
if ($format === self::MODE_MARKDOWN) { |
113
|
|
|
file_put_contents($file, $markDown); |
114
|
|
|
|
115
|
|
|
return; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
file_put_contents($file, $this->convertHtml($title, $markDown)); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
private function mkDir(string $baseDir, string $dirName): string |
122
|
|
|
{ |
123
|
|
|
$dir = sprintf('%s/%s', $baseDir, $dirName); |
124
|
|
|
if (! is_dir($dir)) { |
125
|
|
|
mkdir($dir, 0777, true); // @codeCoverageIgnore |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
return $dir; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
private function getSemanticDoc(AbstractDescriptor $descriptor, string $asd, string $title): string |
132
|
|
|
{ |
133
|
|
|
$descriptorSemantic = $this->getDescriptorInDescriptor($descriptor); |
134
|
|
|
$rt = $this->getRt($descriptor); |
135
|
|
|
$description = ''; |
136
|
|
|
$description .= $this->getDescriptorProp('type', $descriptor); |
137
|
|
|
$description .= $this->getDescriptorProp('title', $descriptor); |
138
|
|
|
$description .= $this->getDescriptorProp('href', $descriptor); |
139
|
|
|
$description .= $this->getDescriptorKeyValue('doc', (string) ($descriptor->doc->value ?? '')); |
140
|
|
|
$description .= $this->getDescriptorProp('def', $descriptor); |
141
|
|
|
$description .= $this->getDescriptorProp('rel', $descriptor); |
142
|
|
|
$description .= $this->getTag($descriptor->tags); |
|
|
|
|
143
|
|
|
$linkRelations = $this->getLinkRelations($descriptor->linkRelations); |
144
|
|
|
$titleHeader = $title ? sprintf('%s: Semantic Descriptor', $title) : 'Semantic Descriptor'; |
145
|
|
|
|
146
|
|
|
return <<<EOT |
147
|
|
|
{$titleHeader} |
148
|
|
|
# {$descriptor->id} |
149
|
|
|
{$description}{$rt}{$linkRelations}{$descriptorSemantic} |
150
|
|
|
--- |
151
|
|
|
|
152
|
|
|
[home](../index.{$this->ext}) | [asd]($asd) |
153
|
|
|
EOT; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
private function getDescriptorProp(string $key, AbstractDescriptor $descriptor): string |
157
|
|
|
{ |
158
|
|
|
if (! property_exists($descriptor, $key) || ! $descriptor->{$key}) { |
159
|
|
|
return ''; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
$value = (string) $descriptor->{$key}; |
163
|
|
|
if ($this->isUrl($value)) { |
164
|
|
|
return " * {$key}: [{$value}]({$value})" . PHP_EOL; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
if ($this->isFragment($value)) { |
168
|
|
|
[, $id] = explode('#', $value); |
169
|
|
|
|
170
|
|
|
return " * {$key}: [{$id}](semantic.{$id}.{$this->ext})" . PHP_EOL; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
return " * {$key}: {$value}" . PHP_EOL; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
private function isUrl(string $text): bool |
177
|
|
|
{ |
178
|
|
|
return filter_var($text, FILTER_VALIDATE_URL) !== false; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
private function isFragment(string $text): bool |
182
|
|
|
{ |
183
|
|
|
return $text[0] === '#'; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
private function getDescriptorKeyValue(string $key, string $value): string |
187
|
|
|
{ |
188
|
|
|
if (! $value) { |
189
|
|
|
return ''; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
return " * {$key}: {$value}" . PHP_EOL; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
private function getRt(AbstractDescriptor $descriptor): string |
196
|
|
|
{ |
197
|
|
|
if ($descriptor instanceof SemanticDescriptor) { |
198
|
|
|
return ''; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
assert($descriptor instanceof TransDescriptor); |
202
|
|
|
|
203
|
|
|
return sprintf(' * rt: [%s](semantic.%s.%s)', $descriptor->rt, $descriptor->rt, $this->ext) . PHP_EOL; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
private function getDescriptorInDescriptor(AbstractDescriptor $descriptor): string |
207
|
|
|
{ |
208
|
|
|
if ($descriptor->descriptor === []) { |
|
|
|
|
209
|
|
|
return ''; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
$descriptors = $this->getInlineDescriptors($descriptor->descriptor); |
|
|
|
|
213
|
|
|
|
214
|
|
|
$table = sprintf(' * descriptor%s%s| id | type | title |%s|---|---|---|%s', PHP_EOL, PHP_EOL, PHP_EOL, PHP_EOL); |
215
|
|
|
foreach ($descriptors as $descriptor) { |
216
|
|
|
$table .= sprintf('| %s | %s | %s |', $descriptor->htmlLink($this->ext), $descriptor->type, $descriptor->title) . PHP_EOL; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
return $table; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* @param non-empty-list<stdClass> $inlineDescriptors |
|
|
|
|
224
|
|
|
* |
225
|
|
|
* @return non-empty-list<AbstractDescriptor> |
|
|
|
|
226
|
|
|
*/ |
227
|
|
|
private function getInlineDescriptors(array $inlineDescriptors): array |
228
|
|
|
{ |
229
|
|
|
$descriptors = []; |
230
|
|
|
foreach ($inlineDescriptors as $descriptor) { |
231
|
|
|
if (isset($descriptor->id)) { |
232
|
|
|
assert(is_string($descriptor->id)); |
233
|
|
|
$descriptors[] = $this->descriptors[$descriptor->id]; |
234
|
|
|
continue; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
assert(is_string($descriptor->href)); |
238
|
|
|
$id = substr($descriptor->href, (int) strpos($descriptor->href, '#') + 1); |
239
|
|
|
assert(isset($this->descriptors[$id])); |
240
|
|
|
|
241
|
|
|
$original = clone $this->descriptors[$id]; |
242
|
|
|
if (isset($descriptor->title)) { |
243
|
|
|
$original->title = (string) $descriptor->title; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
$descriptors[] = $original; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
usort($descriptors, static function (AbstractDescriptor $a, AbstractDescriptor $b): int { |
250
|
|
|
$order = ['semantic' => 0, 'safe' => 1, 'unsafe' => 2, 'idempotent' => 3]; |
251
|
|
|
|
252
|
|
|
return $order[$a->type] <=> $order[$b->type]; |
253
|
|
|
}); |
254
|
|
|
|
255
|
|
|
assert($descriptors !== []); |
256
|
|
|
|
257
|
|
|
return $descriptors; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* @param list<string> $tags |
|
|
|
|
262
|
|
|
*/ |
263
|
|
|
private function getTag(array $tags): string |
264
|
|
|
{ |
265
|
|
|
if ($tags === []) { |
266
|
|
|
return ''; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
return " * tag: {$this->getTagString($tags)}"; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* @param list<string> $tags |
274
|
|
|
*/ |
275
|
|
|
private function getTagString(array $tags): string |
276
|
|
|
{ |
277
|
|
|
$string = []; |
278
|
|
|
foreach ($tags as $tag) { |
279
|
|
|
$string[] = "[{$tag}](tag.{$tag}.{$this->ext})"; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
return implode(', ', $string) . PHP_EOL; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* @param list<string> $descriptorIds |
287
|
|
|
*/ |
288
|
|
|
private function getTagDoc(string $tag, array $descriptorIds, string $title, string $asd): string |
289
|
|
|
{ |
290
|
|
|
$list = ''; |
291
|
|
|
foreach ($descriptorIds as $descriptorId) { |
292
|
|
|
$descriptor = $this->descriptors[$descriptorId]; |
293
|
|
|
$list .= " * {$descriptor->htmlLink($this->ext)}" . PHP_EOL; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
$titleHeader = $title ? sprintf('%s: Tag', $title) : 'Tag'; |
297
|
|
|
|
298
|
|
|
return <<<EOT |
299
|
|
|
{$titleHeader} |
300
|
|
|
# {$tag} |
301
|
|
|
|
302
|
|
|
{$list} |
303
|
|
|
--- |
304
|
|
|
|
305
|
|
|
[home](../index.{$this->ext}) | [asd]({$asd}) | {$tag} |
306
|
|
|
EOT; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
private function getLinkRelations(LinkRelations $linkRelations): string |
310
|
|
|
{ |
311
|
|
|
if ((string) $linkRelations === '') { |
312
|
|
|
return ''; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
return ' * links' . PHP_EOL . $linkRelations . PHP_EOL; |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
|