]> BookStack Code Mirror - api-scripts/blob - php-book-to-static-site/book-to-static.php
e41f3301a5d778834c53dc38a64472543739b86c
[api-scripts] / php-book-to-static-site / book-to-static.php
1 #!/usr/bin/env php
2 <?php
3
4 // API Credentials
5 // You can either provide them as environment variables
6 // or hard-code them in the empty strings below.
7 $apiUrl = getenv('BS_URL') ?: '';
8 $clientId = getenv('BS_TOKEN_ID') ?: '';
9 $clientSecret = getenv('BS_TOKEN_SECRET') ?: '';
10
11 // Output Folder
12 // Can be provided as a arguments when calling the script
13 // or be hard-coded as strings below.
14 $bookSlug = $argv[1] ?? '';
15 $outFolder = $argv[2] ?? './out';
16
17 // Script logic
18 ////////////////
19
20 if (empty($bookSlug) || empty($outFolder)) {
21     errorOut("Both a book slug and output folder must be provided");
22 }
23
24 if (!is_dir($outFolder)) {
25     mkdir($outFolder, 0777, true);
26 }
27
28 $outDir = realpath($outFolder);
29 $book = getBookBySlug($bookSlug);
30
31 if (is_null($book)) {
32     errorOut("Could not find book with the URL slug: {$bookSlug}");
33 }
34
35 $chapters = getAllOfAtListEndpoint("api/chapters", ['filter[book_id]' => $book['id']]);
36 $pages = getAllOfAtListEndpoint("api/pages", ['filter[book_id]' => $book['id']]);
37
38 foreach ($pages as $index => $page) {
39     $pages[$index] = apiGetJson("api/pages/{$page['id']}");
40 }
41
42 if (!is_dir($outDir . "/images")) {
43     mkdir($outDir . "/images", 0777, true);
44 }
45
46 $directBookPages = array_filter($pages, function($page) {
47     return empty($page['chapter_id']);
48 });
49
50 // Create book index file
51 $bookIndex = getBookContent($book, $chapters, $directBookPages);
52 file_put_contents($outDir . "/index.html", $bookIndex);
53
54 foreach ($chapters as $chapter) {
55     $childPages = array_filter($pages, function($page) use ($chapter) {
56         return $page['chapter_id'] == $chapter['id'];
57     });
58     $chapterPage = getChapterContent($chapter, $childPages);
59     file_put_contents($outDir . "/chapter-{$chapter['slug']}.html", $chapterPage);
60
61     foreach ($childPages as $childPage) {
62         $childPageContent = getPageContent($childPage, $chapter);
63         $childPageContent = extractImagesFromHtml($childPageContent);
64         file_put_contents($outDir . "/page-{$childPage['slug']}.html", $childPageContent);
65     }
66 }
67
68 foreach ($directBookPages as $directPage) {
69     $directPageContent = getPageContent($directPage, null);
70     $directPageContent = extractImagesFromHtml($directPageContent);
71     file_put_contents($outDir . "/page-{$directPage['slug']}.html", $directPageContent);
72 }
73
74 function extractImagesFromHtml(string $html): string {
75     global $outDir;
76     $matches = [];
77     preg_match_all('/<img.*?src=["\'](.*?)[\'"].*?>/i', $html, $matches);
78     foreach (array_unique($matches[1] ?? []) as $url) {
79         $image = file_get_contents($url);
80         $name = basename($url);
81         $fileName = $name;
82         $count = 1;
83         while (file_exists($outDir . "/images/" . $fileName)) {
84             $fileName = $count . '-' . $name;
85         }
86         file_put_contents($outDir . "/images/" . $fileName, $image);
87         $html = str_replace($url, "./images/" . $fileName, $html);
88     }
89     return $html;
90 }
91
92 function getImageFile($url): string {
93     global $apiUrl;
94     if (strpos(strtolower($url), strtolower($apiUrl)) === 0) {
95         $url = substr($url, strlen($apiUrl));
96         return apiGet($url);
97     }
98     return file_get_contents($url);
99 }
100
101 function getBookContent(array $book, array $chapters, array $pages): string {
102     $content = "<h1>{$book['name']}</h1>";
103     $content .= "<p>{$book['description']}</p>";
104     $content .= "<hr>";
105     if (count($chapters) > 0) {
106         $content .= "<h3>Chapters</h3><ul>";
107         foreach ($chapters as $chapter) {
108             $content .= "<li><a href='./chapter-{$chapter['slug']}.html'>{$chapter['name']}</a></li>";
109         }
110         $content .= "</ul>";
111     }
112     if (count($pages) > 0) {
113         $content .= "<h3>Pages</h3><ul>";
114         foreach ($pages as $page) {
115             $content .= "<li><a href='./page-{$page['slug']}.html'>{$page['name']}</a></li>";
116         }
117         $content .= "</ul>";
118     }
119     return $content;
120 }
121
122 function getChapterContent(array $chapter, array $pages): string {
123     $content = "<p><a href='./index.html'>Back to book</a></p>";
124     $content .= "<h1>{$chapter['name']}</h1>";
125     $content .= "<p>{$chapter['description']}</p>";
126     $content .= "<hr>";
127     if (count($pages) > 0) {
128         $content .= "<h3>Pages</h3><ul>";
129         foreach ($pages as $page) {
130             $content .= "<li><a href='./page-{$page['slug']}.html'>{$page['name']}</a></li>";
131         }
132         $content .= "</ul>";
133     }
134     return $content;
135 }
136
137 function getPageContent(array $page, ?array $parentChapter): string {
138     if (is_null($parentChapter)) {
139         $content = "<p><a href='./index.html'>Back to book</a></p>";
140     } else {
141         $content = "<p><a href='./chapter-{$parentChapter['slug']}.html'>Back to chapter</a></p>";
142     }
143     $content .= "<h1>{$page['name']}</h1>";
144     $content .= "<div>{$page['html']}</div>";
145     return $content;
146 }
147
148 /**
149  * Get a single book by the slug or return null if not exists.
150  */
151 function getBookBySlug(string $slug): ?array {
152     $endpoint = 'api/books?' . http_build_query(['filter[slug]' => $slug]);
153     $resp = apiGetJson($endpoint);
154     $book = $resp['data'][0] ?? null;
155
156     if (!is_null($book)) {
157         $book = apiGetJson("api/books/{$book['id']}") ?? null;
158     }
159     return $book;
160 }
161
162 /**
163  * Get all books from the system API.
164  */
165 function getAllOfAtListEndpoint(string $endpoint, array $params): array {
166     $count = 100;
167     $offset = 0;
168     $total = 0;
169     $all = [];
170
171     do {
172         $endpoint = $endpoint . '?' . http_build_query(array_merge($params, ['count' => $count, 'offset' => $offset]));
173         $resp = apiGetJson($endpoint);
174
175         $total = $resp['total'] ?? 0;
176         $new = $resp['data'] ?? [];
177         array_push($all, ...$new);
178         $offset += $count;
179     } while ($offset < $total);
180
181     return $all;
182 }
183
184 /**
185  * Make a simple GET HTTP request to the API.
186  */
187 function apiGet(string $endpoint): string {
188     global $apiUrl, $clientId, $clientSecret;
189     $url = rtrim($apiUrl, '/') . '/' . ltrim($endpoint, '/');
190     $opts = ['http' => ['header' => "Authorization: Token {$clientId}:{$clientSecret}"]];
191     $context = stream_context_create($opts);
192     return file_get_contents($url, false, $context);
193 }
194
195 /**
196  * Make a simple GET HTTP request to the API &
197  * decode the JSON response to an array.
198  */
199 function apiGetJson(string $endpoint): array {
200     $data = apiGet($endpoint);
201     return json_decode($data, true);
202 }
203
204 /**
205  * DEBUG: Dump out the given variables and exit.
206  */
207 function dd(...$args) {
208     foreach ($args as $arg) {
209         var_dump($arg);
210     }
211     exit(1);
212 }
213
214 function errorOut(string $text) {
215     echo "ERROR: " .  $text;
216     exit(1);
217 }