|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * PHPCSDevTools, tools for PHP_CodeSniffer sniff developers. |
| 4 | + * |
| 5 | + * @package PHPCSDevTools\GHPages |
| 6 | + * @copyright 2019 PHPCSDevTools Contributors |
| 7 | + * @license https://opensource.org/licenses/LGPL-3.0 LGPL3 |
| 8 | + * @link https://github.com/PHPCSStandards/PHPCSDevTools |
| 9 | + */ |
| 10 | + |
| 11 | +namespace PHPCSDevTools\Build; |
| 12 | + |
| 13 | +use RuntimeException; |
| 14 | + |
| 15 | +/** |
| 16 | + * Prepare the website pages for deploy to GH Pages. |
| 17 | + * |
| 18 | + * {@internal This functionality has a minimum PHP requirement of PHP 7.2.} |
| 19 | + * |
| 20 | + * @internal |
| 21 | + * |
| 22 | + * @phpcs:disable PHPCompatibility.FunctionDeclarations.NewParamTypeDeclarations.stringFound |
| 23 | + * @phpcs:disable PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations.intFound |
| 24 | + * @phpcs:disable PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations.stringFound |
| 25 | + * @phpcs:disable PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations.voidFound |
| 26 | + * @phpcs:disable PHPCompatibility.InitialValue.NewConstantArraysUsingConst.Found |
| 27 | + * @phpcs:disable PHPCompatibility.InitialValue.NewConstantScalarExpressions.constFound |
| 28 | + */ |
| 29 | +final class Website |
| 30 | +{ |
| 31 | + |
| 32 | + /** |
| 33 | + * Path to project root (without trailing slash). |
| 34 | + * |
| 35 | + * @var string |
| 36 | + */ |
| 37 | + const PROJECT_ROOT = __DIR__ . '/../..'; |
| 38 | + |
| 39 | + /** |
| 40 | + * Relative path to target directory off project root (without trailing slash). |
| 41 | + * |
| 42 | + * @var string |
| 43 | + */ |
| 44 | + const TARGET_DIR = '/deploy'; |
| 45 | + |
| 46 | + /** |
| 47 | + * Files to copy. |
| 48 | + * |
| 49 | + * Source should be the relative path from the project root. |
| 50 | + * Target should be the relative path in the target directory. |
| 51 | + * If target is left empty, the target will be the same as the source. |
| 52 | + * |
| 53 | + * @var array<string => string target> |
| 54 | + */ |
| 55 | + const FILES_TO_COPY = [ |
| 56 | + 'README.md' => 'index.md', |
| 57 | + ]; |
| 58 | + |
| 59 | + /** |
| 60 | + * Frontmatter. |
| 61 | + * |
| 62 | + * @var string |
| 63 | + */ |
| 64 | + const FRONTMATTER = '--- |
| 65 | +--- |
| 66 | +'; |
| 67 | + |
| 68 | + /** |
| 69 | + * Resolved path to project root (with trailing slash). |
| 70 | + * |
| 71 | + * @var string |
| 72 | + */ |
| 73 | + private $realRoot; |
| 74 | + |
| 75 | + /** |
| 76 | + * Resolved path to target directory (with trailing slash). |
| 77 | + * |
| 78 | + * @var string |
| 79 | + */ |
| 80 | + private $realTarget; |
| 81 | + |
| 82 | + /** |
| 83 | + * Constructor |
| 84 | + * |
| 85 | + * @return void |
| 86 | + */ |
| 87 | + public function __construct() |
| 88 | + { |
| 89 | + // Check if the target directory exists and if not, create it. |
| 90 | + $targetDir = self::PROJECT_ROOT . self::TARGET_DIR; |
| 91 | + |
| 92 | + if (@\is_dir($targetDir) === false) { |
| 93 | + if (@\mkdir($targetDir, 0777, true) === false) { |
| 94 | + throw new RuntimeException(\sprintf('Failed to create the %s directory.', $targetDir)); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + $realPath = \realpath($targetDir); |
| 99 | + if ($realPath === false) { |
| 100 | + throw new RuntimeException(\sprintf('Failed to find the %s directory.', $targetDir)); |
| 101 | + } |
| 102 | + |
| 103 | + $this->realRoot = \realpath(self::PROJECT_ROOT) . '/'; |
| 104 | + $this->realTarget = $realPath . '/'; |
| 105 | + } |
| 106 | + |
| 107 | + /** |
| 108 | + * Run the transformation. |
| 109 | + * |
| 110 | + * @return int Exit code. |
| 111 | + */ |
| 112 | + public function run(): int |
| 113 | + { |
| 114 | + $exitcode = 0; |
| 115 | + |
| 116 | + try { |
| 117 | + $this->copyFiles(); |
| 118 | + $this->transformIndex(); |
| 119 | + } catch (RuntimeException $e) { |
| 120 | + echo 'ERROR: ', $e->getMessage(), \PHP_EOL; |
| 121 | + $exitcode = 1; |
| 122 | + } |
| 123 | + |
| 124 | + return $exitcode; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Copy files to the target directory. |
| 129 | + * |
| 130 | + * @return void |
| 131 | + */ |
| 132 | + private function copyFiles(): void |
| 133 | + { |
| 134 | + foreach (self::FILES_TO_COPY as $source => $target) { |
| 135 | + $source = $this->realRoot . $source; |
| 136 | + if (empty($target)) { |
| 137 | + $target = $this->realTarget . $source; |
| 138 | + } else { |
| 139 | + $target = $this->realTarget . $target; |
| 140 | + } |
| 141 | + |
| 142 | + // Bit round-about way of copying the files, but we need to make sure the target dir exists. |
| 143 | + $contents = $this->getContents($source); |
| 144 | + $this->putContents($target, $contents); |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + /** |
| 149 | + * Transform the README to a usable homepage. |
| 150 | + * |
| 151 | + * - Remove the title and subtitle as those would become duplicate. |
| 152 | + * - Remove most of the badges, except for the first three. |
| 153 | + * - Transform those badges into HTML. |
| 154 | + * - Add frontmatter. |
| 155 | + * |
| 156 | + * @return void |
| 157 | + * |
| 158 | + * @throws \RuntimeException When any of the regexes do not yield any results. |
| 159 | + */ |
| 160 | + private function transformIndex(): void |
| 161 | + { |
| 162 | + // Read the file. |
| 163 | + $target = $this->realTarget . '/index.md'; |
| 164 | + $contents = $this->getContents($target); |
| 165 | + |
| 166 | + // Grab the start of the document. |
| 167 | + $matched = \preg_match('`^(.+)\* \[Installation\]`s', $contents, $matches); |
| 168 | + if ($matched !== 1) { |
| 169 | + throw new RuntimeException('Failed to match start of document. Adjust the regex'); |
| 170 | + } |
| 171 | + |
| 172 | + $startOfDoc = $matches[1]; |
| 173 | + |
| 174 | + // Grab the first few badges from the start of the document. |
| 175 | + $matched = \preg_match( |
| 176 | + '`((?:\[!\[[^\]]+\]\([^\)]+\)\]\([^\)]+\)[\n\r]+)+):construction:`', |
| 177 | + $startOfDoc, |
| 178 | + $matches |
| 179 | + ); |
| 180 | + if ($matched !== 1) { |
| 181 | + throw new RuntimeException('Failed to match badges. Adjust the regex'); |
| 182 | + } |
| 183 | + |
| 184 | + $badges = \explode("\n", $matches[1]); |
| 185 | + $badges = \array_filter($badges); |
| 186 | + $badges = \array_map([$this, 'mdBadgeToHtml'], $badges); |
| 187 | + $badges = \implode("\n ", $badges); |
| 188 | + |
| 189 | + $replacement = \sprintf( |
| 190 | + '%s |
| 191 | +
|
| 192 | +<div id="badges" aria-hidden="true"> |
| 193 | +
|
| 194 | +%s |
| 195 | +
|
| 196 | +</div> |
| 197 | +
|
| 198 | +', |
| 199 | + self::FRONTMATTER, |
| 200 | + ' ' . $badges |
| 201 | + ); |
| 202 | + |
| 203 | + $contents = \str_replace($startOfDoc, $replacement, $contents); |
| 204 | + |
| 205 | + $this->putContents($target, $contents); |
| 206 | + } |
| 207 | + |
| 208 | + /** |
| 209 | + * Transform markdown badges into HTML badges. |
| 210 | + * |
| 211 | + * Jekyll runs into trouble doing this when we also want to keep the wrapper div with aria-hidden="true". |
| 212 | + * |
| 213 | + * @param string $mdBadge Markdown badge code. |
| 214 | + * |
| 215 | + * @return string |
| 216 | + */ |
| 217 | + private function mdBadgeToHtml(string $mdBadge): string |
| 218 | + { |
| 219 | + $mdBadge = trim($mdBadge); |
| 220 | + |
| 221 | + $matched = \preg_match( |
| 222 | + '`^\[!\[(?<alt>[^\]]+)\]\((?<imgurl>[^\)]+)\)\]\((?<href>[^\)]+)\)$`', |
| 223 | + $mdBadge, |
| 224 | + $matches |
| 225 | + ); |
| 226 | + if ($matched !== 1) { |
| 227 | + throw new RuntimeException(\sprintf('Failed to parse the badge. Adjust the regex. Received: %s', $mdBadge)); |
| 228 | + } |
| 229 | + |
| 230 | + return \sprintf( |
| 231 | + '<a href="%s"><img src="%s" alt="%s" class="badge"></a>', |
| 232 | + $matches['href'], |
| 233 | + $matches['imgurl'], |
| 234 | + $matches['alt'] |
| 235 | + ); |
| 236 | + } |
| 237 | + |
| 238 | + /** |
| 239 | + * Retrieve the contents of a file. |
| 240 | + * |
| 241 | + * @param string $source Path to the source file. |
| 242 | + * |
| 243 | + * @return string |
| 244 | + * |
| 245 | + * @throws \RuntimeException When the contents of the file could not be retrieved. |
| 246 | + */ |
| 247 | + private function getContents(string $source): string |
| 248 | + { |
| 249 | + $contents = \file_get_contents($source); |
| 250 | + if (!$contents) { |
| 251 | + throw new RuntimeException(\sprintf('Failed to read doc file: %s', $source)); |
| 252 | + } |
| 253 | + |
| 254 | + return $contents; |
| 255 | + } |
| 256 | + |
| 257 | + /** |
| 258 | + * Write a string to a file. |
| 259 | + * |
| 260 | + * @param string $target Path to the target file. |
| 261 | + * @param string $contents File contents to write. |
| 262 | + * |
| 263 | + * @return void |
| 264 | + * |
| 265 | + * @throws \RuntimeException When the target directory could not be created. |
| 266 | + * @throws \RuntimeException When the file could not be written to the target directory. |
| 267 | + */ |
| 268 | + private function putContents(string $target, string $contents): void |
| 269 | + { |
| 270 | + // Check if the target directory exists and if not, create it. |
| 271 | + $targetDir = \dirname($target); |
| 272 | + |
| 273 | + if (@\is_dir($targetDir) === false) { |
| 274 | + if (@\mkdir($targetDir, 0777, true) === false) { |
| 275 | + throw new RuntimeException(\sprintf('Failed to create the %s directory.', $targetDir)); |
| 276 | + } |
| 277 | + } |
| 278 | + |
| 279 | + // Make sure the file always ends on a new line. |
| 280 | + $contents = \rtrim($contents) . "\n"; |
| 281 | + if (\file_put_contents($target, $contents) === false) { |
| 282 | + throw new RuntimeException(\sprintf('Failed to write to target location: %s', $target)); |
| 283 | + } |
| 284 | + } |
| 285 | +} |
0 commit comments