From 86b21d58a501a2fdfbc0882d99613732bb04303c Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 14:50:42 +0200 Subject: [PATCH 01/12] Fix wrong page type names --- docs/creating-content/documentation-pages.md | 2 +- docs/creating-content/static-pages.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/creating-content/documentation-pages.md b/docs/creating-content/documentation-pages.md index b0b08f35354..22719ed17d6 100644 --- a/docs/creating-content/documentation-pages.md +++ b/docs/creating-content/documentation-pages.md @@ -24,7 +24,7 @@ saving you time and effort. For more information about this feature, see the [ex ### Best Practices and Hyde Expectations Since Hyde does a lot of things automatically, there are some things you may need -to keep in mind when creating blog posts so that you don't get unexpected results. +to keep in mind when creating documentation pages so that you don't get unexpected results. #### Filenames diff --git a/docs/creating-content/static-pages.md b/docs/creating-content/static-pages.md index ea130f3df7a..8eef4a50093 100644 --- a/docs/creating-content/static-pages.md +++ b/docs/creating-content/static-pages.md @@ -18,7 +18,7 @@ Let's start with the basics. ### Best Practices and Hyde Expectations Since Hyde does a lot of things automatically, there are some things you may need -to keep in mind when creating blog posts so that you don't get unexpected results. +to keep in mind when creating pages so that you don't get unexpected results. #### Filenames From b2b5c8113f62abdad938f1976706f20efa011aa3 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 13:44:01 +0200 Subject: [PATCH 02/12] Refactor the Markdown formatter script with Claude Confirmed to have no effect on the results. --- monorepo/scripts/docs/MarkdownFormatter.php | 722 ++++++++++---------- 1 file changed, 378 insertions(+), 344 deletions(-) diff --git a/monorepo/scripts/docs/MarkdownFormatter.php b/monorepo/scripts/docs/MarkdownFormatter.php index a4200fae602..3a34c0e70ec 100644 --- a/monorepo/scripts/docs/MarkdownFormatter.php +++ b/monorepo/scripts/docs/MarkdownFormatter.php @@ -17,8 +17,7 @@ $links = []; $warnings = []; -// Buffer headings so we can check for style -$headings = []; // [filename => [line => heading]] +$headings = []; $checksHeadings = false; $fixesHeadings = false; @@ -38,91 +37,133 @@ public function __construct(string $input, string $filename = 'Input') protected function run(): void { - $stream = $this->input; - $filename = $this->filename; + $text = $this->normalizeLineEndings($this->input); - $text = $stream; + if ($this->isEmptyContent($text)) { + return; + } + + $lines = explode("\n", $text); + $newLines = $this->processLines($lines); + + $this->output = $this->finalizeOutput($newLines); + } + + protected function normalizeLineEndings(string $text): string + { $text = str_replace("\r\n", "\n", $text); - $text = str_replace("\t", ' ', $text); + return str_replace("\t", ' ', $text); + } + + protected function isEmptyContent(string $text): bool + { if (empty(trim($text))) { - // Warn global $warnings; - $warnings[] = "File $filename is empty"; + $warnings[] = "File {$this->filename} is empty"; - return; + return true; } - $lines = explode("\n", $text); - $new_lines = []; + return false; + } - $last_line = ''; - $was_last_line_heading = false; - $is_inside_fenced_code_block = false; - $is_inside_fenced_fenced_code_block = false; + /** + * @param string[] $lines + * @return string[] + */ + protected function processLines(array $lines): array + { + $newLines = []; + $lastLine = ''; + $wasLastLineHeading = false; + $isInsideFencedCodeBlock = false; + $isInsideFencedFencedCodeBlock = false; $firstHeadingLevel = null; + foreach ($lines as $index => $line) { global $linesCounted; $linesCounted++; - /** Normalization */ - - // Remove multiple empty lines - if (trim($line) == '' && trim($last_line) == '') { + if ($this->shouldSkipLine($line, $lastLine)) { continue; } - // Make sure there is a space after headings - if ($was_last_line_heading && trim($line) != '') { - $new_lines[] = ''; - } + $line = $this->processLine($line, $lastLine, $wasLastLineHeading, $isInsideFencedCodeBlock, $isInsideFencedFencedCodeBlock, $firstHeadingLevel, $index); - // Make sure there is exactly one empty line before any heading - if (! $is_inside_fenced_code_block && preg_match('/^#{1,6} /', $line) && trim($last_line) != '') { - $new_lines[] = ''; - } + $newLines[] = $line; + $lastLine = $line; + $wasLastLineHeading = $this->isHeading($line); + $isInsideFencedCodeBlock = $this->updateFencedCodeBlockStatus($line, $isInsideFencedCodeBlock); + $isInsideFencedFencedCodeBlock = $this->updateFencedFencedCodeBlockStatus($line, $isInsideFencedFencedCodeBlock); + $firstHeadingLevel = $this->updateFirstHeadingLevel($line, $firstHeadingLevel, $index); + } - if ($firstHeadingLevel === null && str_starts_with($line, '# ')) { - $firstHeadingLevel = $index; - } + return $newLines; + } - // Check if line is a heading - if (preg_match('/^#{1,6} /', $line)) { - $was_last_line_heading = true; - global $headings; - $headings[$filename][$index + 1] = $line; - } else { - $was_last_line_heading = false; - } + protected function shouldSkipLine(string $line, string $lastLine): bool + { + return trim($line) === '' && trim($lastLine) === ''; + } - // Make sure there is a space before opening a fenced code block (search for ```language) - if (str_starts_with($line, '```') && $line !== '```' && trim($last_line) != '') { - if (! $is_inside_fenced_fenced_code_block) { - $new_lines[] = ''; - } - } + protected function processLine(string $line, string $lastLine, bool $wasLastLineHeading, bool $isInsideFencedCodeBlock, bool $isInsideFencedFencedCodeBlock, ?int $firstHeadingLevel, int $index): string + { + if ($wasLastLineHeading && trim($line) !== '') { + $line = "\n".$line; + } - // Check if line is a fenced code block - if (str_starts_with($line, '``')) { - $is_inside_fenced_code_block = ! $is_inside_fenced_code_block; - } + if (! $isInsideFencedCodeBlock && $this->isHeading($line) && trim($lastLine) !== '') { + $line = "\n".$line; + } - // Check if line is an escaped fenced code block - if (str_starts_with($line, '````')) { - $is_inside_fenced_fenced_code_block = ! $is_inside_fenced_fenced_code_block; - } + if ($this->shouldAddEmptyLineBeforeFencedCodeBlock($line, $lastLine, $isInsideFencedFencedCodeBlock)) { + $line = "\n".$line; + } + + $this->processHeading($line, $index); + + return rtrim($line); + } + + protected function isHeading(string $line): bool + { + return preg_match('/^#{1,6} /', $line) === 1; + } + + protected function updateFencedCodeBlockStatus(string $line, bool $currentStatus): bool + { + return str_starts_with($line, '``') ? ! $currentStatus : $currentStatus; + } + + protected function updateFencedFencedCodeBlockStatus(string $line, bool $currentStatus): bool + { + return str_starts_with($line, '````') ? ! $currentStatus : $currentStatus; + } - // Remove trailing spaces - $line = rtrim($line); + protected function updateFirstHeadingLevel(string $line, ?int $currentLevel, int $index): ?int + { + return $currentLevel === null && str_starts_with($line, '# ') ? $index : $currentLevel; + } - $new_lines[] = $line; - $last_line = $line; + protected function shouldAddEmptyLineBeforeFencedCodeBlock(string $line, string $lastLine, bool $isInsideFencedFencedCodeBlock): bool + { + return str_starts_with($line, '```') && $line !== '```' && trim($lastLine) !== '' && ! $isInsideFencedFencedCodeBlock; + } + + protected function processHeading(string $line, int $index): void + { + if ($this->isHeading($line)) { + global $headings; + $headings[$this->filename][$index + 1] = $line; } + } - $new_content = implode("\n", $new_lines); - $new_content = trim($new_content)."\n"; + protected function finalizeOutput(array $newLines): string + { + $newContent = implode("\n", $newLines); - $this->output = $new_content; + return trim($newContent)."\n"; } public function getOutput(): string @@ -133,11 +174,9 @@ public function getOutput(): string function lint(string $filename): void { - /** Linting */ $text = file_get_contents($filename); if (empty(trim($text))) { - // Warn global $warnings; $warnings[] = "File $filename is empty"; @@ -145,206 +184,241 @@ function lint(string $filename): void } $lines = explode("\n", $text); - $is_inside_fenced_code_block = false; + $isInsideFencedCodeBlock = false; + foreach ($lines as $index => $line) { - // Check if line is a fenced code block - if (str_starts_with($line, '``')) { - $is_inside_fenced_code_block = ! $is_inside_fenced_code_block; - } + $isInsideFencedCodeBlock = processLine($filename, $line, $index, $isInsideFencedCodeBlock); + } +} - // if not inside fenced code block - if (! $is_inside_fenced_code_block) { - // Add any links to buffer, so we can check them later - preg_match_all('/\[([^\[]+)]\((.*)\)/', $line, $matches); - if (count($matches) > 0) { - foreach ($matches[2] as $match) { - // If link is for an anchor, prefix the filename - if (str_starts_with($match, '#')) { - $match = 'ANCHOR_'.basename($filename).$match; - } - - global $links; - $links[] = [ - 'filename' => $filename, - 'line' => $index + 1, - 'link' => $match, - ]; - } - } +function processLine(string $filename, string $line, int $index, bool $isInsideFencedCodeBlock): bool +{ + $isInsideFencedCodeBlock = updateFencedCodeBlockStatus($line, $isInsideFencedCodeBlock); + + if (! $isInsideFencedCodeBlock) { + processLinks($filename, $line, $index); + checkInlineCode($filename, $line, $index); + checkCommandSignatures($filename, $line, $index); + checkLineLengthAndLegacyMarkers($filename, $line, $index); + checkLegacyTerms($filename, $line, $index); + } - // Check for un-backtick-ed inline code - // If line contains $ - if (str_contains($line, '$') && ! str_contains($line, '[Blade]:') && ! str_contains($line, '$ php')) { - // Check character before the $ is not a backtick - $pos = strpos($line, '$'); - if ($pos > 0) { - $charBefore = substr($line, $pos - 1, 1); - if ($charBefore !== '`') { - global $warnings; - $warnings['Inline code'][] = sprintf('Unformatted inline code found in %s:%s', $filename, $index + 1); - } - } - } - // If line contains command - if (str_contains($line, 'php hyde') && ! str_contains($line, '[Blade]:') && ! str_contains($line, '$ php')) { - // Check character before the php hyde is not a backtick - $pos = strpos($line, 'php hyde'); - if ($pos > 0) { - $charBefore = substr($line, $pos - 1, 1); - if ($charBefore !== '`') { - global $warnings; - $warnings['Inline code'][] = sprintf('Unformatted inline command found in %s:%s', $filename, $index + 1); - } - } - } - // If word ends in .php - if (str_contains($line, '.php') && ! str_contains($line, '[Blade]:') && ! str_contains($line, '$ php') && ! str_contains($line, 'http') && ! str_contains(strtolower($line), 'filepath')) { - // Check character after the .php is not a backtick - $pos = strpos($line, '.php'); - if ($pos > 0) { - $charAfter = substr($line, $pos + 4, 1); - if ($charAfter !== '`') { - global $warnings; - $warnings['Inline code'][] = sprintf('Unformatted inline filename found in %s:%s', $filename, $index + 1); - } - } - } + return $isInsideFencedCodeBlock; +} - // If word ends in .json - if (str_contains($line, '.json') && ! str_contains($line, '[Blade]:') && ! str_contains($line, '$ json') && ! str_contains($line, 'http') && ! str_contains(strtolower($line), 'filepath')) { - // Check character after the .json is not a backtick - $pos = strpos($line, '.json'); - if ($pos > 0) { - $charAfter = substr($line, $pos + 5, 1); - if ($charAfter !== '`') { - global $warnings; - $warnings['Inline code'][] = sprintf('Unformatted inline filename found in %s:%s', $filename, $index + 1); - } - } - } - // if word ends with () - if (str_contains($line, '()') && ! str_contains($line, '[Blade]:')) { - // Check character after the () is not a backtick - $pos = strpos($line, '()'); - if ($pos > 0) { - $charAfter = substr($line, $pos + 2, 1); - if ($charAfter !== '`') { - global $warnings; - $warnings['Inline code'][] = sprintf('Unformatted inline function found in %s:%s', $filename, $index + 1); - } - } - } +function updateFencedCodeBlockStatus(string $line, bool $currentStatus): bool +{ + return str_starts_with($line, '``') ? ! $currentStatus : $currentStatus; +} - // Check for invalid command signatures - if (str_contains($line, 'php hyde')) { - // Extract signature from line - $start = strpos($line, 'php hyde'); - $substr = substr($line, $start); - $explode = explode(' ', $substr, 3); - $signature = $explode[0].' '.$explode[1].' '.$explode[2]; - $end = strpos($signature, '`'); - if ($end === false) { - $end = strpos($signature, '<'); - if ($end === false) { - $end = strlen($signature); - } - } - $signature = substr($signature, 0, $end); - $signatures = getSignatures(); - if (! in_array($signature, $signatures)) { - global $warnings; - $warnings['Invalid command signatures'][] = sprintf('Invalid command signature \'%s\' found in %s:%s', $signature, $filename, $index + 1); - } - } +function checkInlineCode(string $filename, string $line, int $index): void +{ + $inlineCodePatterns = [ + '/\$/' => 'Unformatted inline code', + '/php hyde/' => 'Unformatted inline command', + '/\.php/' => 'Unformatted inline filename', + '/\.json/' => 'Unformatted inline filename', + '/\(\)/' => 'Unformatted inline function', + ]; + + foreach ($inlineCodePatterns as $pattern => $warningType) { + if (preg_match($pattern, $line) && ! isExcludedFromInlineCodeCheck($line)) { + global $warnings; + $warnings['Inline code'][] = sprintf('%s found in %s:%s', $warningType, $filename, $index + 1); } + } +} + +function isExcludedFromInlineCodeCheck(string $line): bool +{ + return str_contains($line, '[Blade]:') || str_contains($line, '$ php') || str_contains($line, 'http') || str_contains(strtolower($line), 'filepath'); +} - // Check if line is too long - if (strlen($line) > 120) { +function checkCommandSignatures(string $filename, string $line, int $index): void +{ + if (str_contains($line, 'php hyde')) { + $signature = extractCommandSignature($line); + $signatures = getSignatures(); + if (! in_array($signature, $signatures)) { global $warnings; - // $warnings[] = 'Line '.$linesCounted.' in file '.$filename.' is too long'; + $warnings['Invalid command signatures'][] = sprintf('Invalid command signature \'%s\' found in %s:%s', $signature, $filename, $index + 1); } + } +} - // Warn if documentation contains legacy markers (experimental, beta, etc) - $markers = ['experimental', 'beta', 'alpha', 'v0.']; - foreach ($markers as $marker) { - if (str_contains($line, $marker)) { - global $warnings; - $warnings['Legacy markers'][] = sprintf('Legacy marker found in %s:%s Found "%s"', $filename, $index + 1, $marker); - } +function extractCommandSignature(string $line): string +{ + $start = strpos($line, 'php hyde'); + $substr = substr($line, $start); + $explode = explode(' ', $substr, 3); + $signature = $explode[0].' '.$explode[1].' '.$explode[2]; + $end = strpos($signature, '`') ?: strpos($signature, '<') ?: strlen($signature); + + return substr($signature, 0, $end); +} + +function checkLineLengthAndLegacyMarkers(string $filename, string $line, int $index): void +{ + // Todo: Implement line length check + $markers = ['experimental', 'beta', 'alpha', 'v0.']; + foreach ($markers as $marker) { + if (str_contains($line, $marker)) { + global $warnings; + $warnings['Legacy markers'][] = sprintf('Legacy marker found in %s:%s Found "%s"', $filename, $index + 1, $marker); } + } +} - // Warn when legacy terms are used (for example slug instead of identifier/route key) - $legacyTerms = [ - 'slug' => '"identifier" or "route key"', - 'slugs' => '"identifiers" or "route keys"', - ]; +function checkLegacyTerms(string $filename, string $line, int $index): void +{ + $legacyTerms = [ + 'slug' => '"identifier" or "route key"', + 'slugs' => '"identifiers" or "route keys"', + ]; - foreach ($legacyTerms as $legacyTerm => $newTerm) { - if (str_contains(strtolower($line), $legacyTerm)) { - global $warnings; - $warnings['Legacy terms'][] = sprintf('Legacy term found in %s:%s Found "%s", should be %s', $filename, $index + 1, $legacyTerm, $newTerm); - } + foreach ($legacyTerms as $legacyTerm => $newTerm) { + if (str_contains(strtolower($line), $legacyTerm)) { + global $warnings; + $warnings['Legacy terms'][] = sprintf('Legacy term found in %s:%s Found "%s", should be %s', $filename, $index + 1, $legacyTerm, $newTerm); } } } -function find_markdown_files($dir): array +/** + * @return string[] + */ +function find_markdown_files(string $dir): array { - $markdown_files = []; + $markdownFiles = []; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); foreach ($iterator as $file) { - // Skip _data directory if (str_contains($file->getPathname(), '_data')) { continue; } - if ($file->isFile() && strtolower($file->getExtension()) == 'md') { - $markdown_files[] = realpath($file->getPathname()); + if ($file->isFile() && strtolower($file->getExtension()) === 'md') { + $markdownFiles[] = realpath($file->getPathname()); } } - return $markdown_files; + return $markdownFiles; } function handle_file(string $file): void { - echo 'Handling '.$file."\n"; + echo "Handling $file\n"; normalize_lines($file); lint($file); } -function normalize_lines($filename): void +function normalize_lines(string $filename): void { $stream = file_get_contents($filename); $formatter = new MarkdownFormatter($stream, $filename); - $new_content = $formatter->getOutput(); + $newContent = $formatter->getOutput(); - file_put_contents($filename, $new_content); + file_put_contents($filename, $newContent); - if ($new_content !== $stream) { + if ($newContent !== $stream) { global $filesChanged; $filesChanged++; } } -$dir = __DIR__.'/../../../docs'; -$markdownFiles = find_markdown_files($dir); +/** + * @return string[] + */ +function getSignatures(): array +{ + static $signatures = null; -foreach ($markdownFiles as $file) { - handle_file($file); + if ($signatures === null) { + $cache = __DIR__.'/../cache/hyde-signatures.php'; + if (file_exists($cache)) { + $signatures = include $cache; + } else { + $signatures = [ + 'php hyde list', + 'php hyde change:sourceDirectory', + ]; + $commandRaw = shell_exec('cd ../../../ && php hyde list --raw'); + foreach (explode("\n", $commandRaw) as $command) { + $command = Str::before($command, ' '); + $signatures[] = trim('php hyde '.$command); + } + file_put_contents($cache, ' $fileHeadings) { + foreach ($fileHeadings as $heading) { + $headingLevel = substr_count($heading, '#'); + $headingText = trim(str_replace('#', '', $heading)); + + if (str_word_count($headingText) === 1 || str_word_count($headingText) > 5) { + continue; + } + + $expectedCase = $headingLevel < 3 ? Hyde\make_title($headingText) : Str::ucfirst($headingText); + $expectedCase = adjustCaseForSpecialWords($expectedCase); + + if ($headingText !== $expectedCase) { + $caseType = $headingLevel < 3 ? 'title' : 'sentence'; + $warnings['Headings'][] = "Heading '$headingText' should be $caseType case in $filename (expected '$expectedCase')"; + + if ($fixesHeadings) { + fixHeading($filename, $heading, $headingLevel, $expectedCase); + } + } + } + } +} + +function adjustCaseForSpecialWords(string $text): string +{ + $alwaysUppercase = ['PHP', 'HTML', 'CLI', 'API', 'YAML', 'XML', 'RSS', 'HydeKernel', 'GitHub']; + $alwaysLowercase = ['to', 'it']; + + $text = str_ireplace($alwaysUppercase, $alwaysUppercase, $text); + + return str_ireplace($alwaysLowercase, $alwaysLowercase, $text); } +function fixHeading(string $filename, string $heading, int $headingLevel, string $expectedCase): void +{ + $headingHashes = str_repeat('#', $headingLevel); + $newHeading = "$headingHashes $expectedCase"; -// Just to make PhpStorm happy -$links[] = [ - 'filename' => '', - 'line' => 1, - 'link' => '', -]; + $newContent = file_get_contents($filename); + $newContent = str_replace($heading, $newHeading, $newContent); + file_put_contents($filename, $newContent); + + echo "Fixed heading '$heading' to '$newHeading' in $filename\n"; +} + +function processLinks(): void +{ + global $links, $warnings; + + if (empty($links)) { + return; + } -if (count($links) > 0) { $uniqueLinks = []; foreach ($links as $data) { @@ -353,11 +427,7 @@ function normalize_lines($filename): void $line = $data['line']; if (str_starts_with($link, 'http')) { - // Check for outdated links - // laravel.com/docs/9.x - if (str_contains($link, 'laravel.com/docs/9.x')) { - $warnings['Outdated links'][] = "Outdated documentation link to $link found in $filename:$line"; - } + checkOutdatedLink($link, $filename, $line); continue; } @@ -365,143 +435,76 @@ function normalize_lines($filename): void continue; } - // Remove hash for anchors - $link = explode('#', $link)[0]; - // Remove anything before spaces (image alt text) - $link = explode(' ', $link)[0]; - // Trim any non-alphanumeric characters from the end of the link - $link = rtrim($link, '.,;:!?)'); + $link = cleanLink($link); if (! str_starts_with($link, 'ANCHOR_')) { - // Add to new unique array $uniqueLinks[$link] = "$filename:$line"; } } + validateLinks($uniqueLinks); +} + +function checkOutdatedLink(string $link, string $filename, int $line): void +{ + global $warnings; + + if (str_contains($link, 'laravel.com/docs/9.x')) { + $warnings['Outdated links'][] = "Outdated documentation link to $link found in $filename:$line"; + } +} + +function cleanLink(string $link): string +{ + $link = explode('#', $link)[0]; + $link = explode(' ', $link)[0]; + + return rtrim($link, '.,;:!?)'); +} + +function validateLinks(array $uniqueLinks): void +{ + global $warnings; + $base = __DIR__.'/../../../docs'; - // find all directories in the docs folder $directories = array_filter(glob($base.'/*'), 'is_dir'); foreach ($uniqueLinks as $link => $location) { - // Check uses pretty urls if (str_ends_with($link, '.html')) { $warnings['Bad links'][] = "Link to $link in $location should not use .html extension"; continue; } - // Check does not end with .md if (str_ends_with($link, '.md')) { $warnings['Bad links'][] = "Link to $link in $location must not use .md extension"; continue; } - // Check if file exists - if (! file_exists($base.'/'.$link)) { - $hasMatch = false; - foreach ($directories as $directory) { - if (file_exists($directory.'/'.$link.'.md')) { - $hasMatch = true; - break; - } - } - - if (! $hasMatch) { - // Check that link is not for search (dynamic page) - if (! str_contains($link, 'search')) { - $warnings['Broken links'][] = "Broken link to $link found in $location"; - } - } + if (! file_exists($base.'/'.$link) && ! linkExistsInDirectories($link, $directories) && ! str_contains($link, 'search')) { + $warnings['Broken links'][] = "Broken link to $link found in $location"; } } } -function getSignatures(): array +function linkExistsInDirectories(string $link, array $directories): bool { - static $signatures = null; - - if ($signatures === null) { - $cache = __DIR__.'/../cache/hyde-signatures.php'; - if (file_exists($cache)) { - $signatures = include $cache; - } else { - $signatures = [ - // Adds any hidden commands we know exist - 'php hyde list', - 'php hyde change:sourceDirectory', - ]; - $commandRaw = shell_exec('cd ../../../ && php hyde list --raw'); - foreach (explode("\n", $commandRaw) as $command) { - $command = Str::before($command, ' '); - $signatures[] = trim('php hyde '.$command); - } - file_put_contents($cache, ' $fileHeadings) { - $headingLevels = []; - foreach ($fileHeadings as $heading) { - $headingLevel = substr_count($heading, '#'); - $headingLevels[] = $headingLevel; - - // Check for style: 1-2 headings should be title case, 3+ should be sentence case - $headingText = trim(str_replace('#', '', $heading)); - $titleCase = Hyde\make_title($headingText); - $alwaysUppercase = ['PHP', 'HTML', 'CLI', 'API', 'YAML', 'XML', 'RSS', 'HydeKernel', 'GitHub']; - $alwaysLowercase = ['to', 'it']; - $titleCase = str_ireplace($alwaysUppercase, $alwaysUppercase, $titleCase); - $titleCase = str_ireplace($alwaysLowercase, $alwaysLowercase, $titleCase); - - $isTitleCase = $headingText === $titleCase; - $sentenceCase = Str::ucfirst($headingText); - $isSentenceCase = $headingText === $sentenceCase; - - // If it's just one word, or if it's more than 5 words, we can ignore it - $canIgnore = str_word_count($headingText) === 1 || str_word_count($headingText) > 5; - - if ($canIgnore) { - continue; - } - - $something = false; - if ($headingLevel < 3) { - if (! $isTitleCase) { - $warnings['Headings'][] = "Heading '$headingText' should be title case in $filename (expected '$titleCase')"; - } - } else { - if (! $isSentenceCase) { - $warnings['Headings'][] = "Heading '$headingText' should be sentence case in $filename (expected '$sentenceCase')"; - } - } - - if ($fixesHeadings) { - // Replace the heading with the expected case - - $headingHashes = str_repeat('#', $headingLevel); - $useCase = $headingLevel < 3 ? $titleCase : $sentenceCase; - $newHeading = "$headingHashes $useCase"; - - $newContent = file_get_contents($filename); - $newContent = str_replace($heading, $newHeading, $newContent); - file_put_contents($filename, $newContent); +function displayWarnings(): void +{ + global $warnings; - echo "Fixed heading '$headingText' to '$newHeading' in $filename\n"; - } - } + if (empty($warnings)) { + return; } -} -if (count($warnings) > 0) { echo "\n\033[31mWarnings:\033[0m \033[33m".count($warnings, COUNT_RECURSIVE) - count($warnings)." found \033[0m \n"; foreach ($warnings as $type => $messages) { echo "\n\033[33m$type:\033[0m \n"; @@ -511,38 +514,69 @@ function getSignatures(): array } } -$time = round((microtime(true) - $timeStart) * 1000, 2); -$linesTransformed = number_format($linesCounted); -$fileCount = count($markdownFiles); +function displaySummary(): void +{ + global $timeStart, $linesCounted, $markdownFiles, $filesChanged, $warnings; + + $time = round((microtime(true) - $timeStart) * 1000, 2); + $linesTransformed = number_format($linesCounted); + $fileCount = count($markdownFiles); -echo "\n\n\033[32mAll done!\033[0m Formatted, normalized, and validated $linesTransformed lines of Markdown in $fileCount files in {$time}ms\n"; + echo "\n\n\033[32mAll done!\033[0m Formatted, normalized, and validated $linesTransformed lines of Markdown in $fileCount files in {$time}ms\n"; + + if ($filesChanged > 0) { + echo "\n\033[32m$filesChanged files were changed.\033[0m "; + } else { + echo "\n\033[32mNo files were changed.\033[0m "; + } -if ($filesChanged > 0) { - echo "\n\033[32m$filesChanged files were changed.\033[0m "; -} else { - echo "\n\033[32mNo files were changed.\033[0m "; + $warningCount = count($warnings, COUNT_RECURSIVE) - count($warnings); + displayWarningComparison($warningCount); } -$warningCount = count($warnings, COUNT_RECURSIVE) - count($warnings); -if ($warningCount > 0) { - echo sprintf("\033[33m%s %s found.\033[0m", $warningCount, $warningCount === 1 ? 'warning' : 'warnings'); - if (file_exists(__DIR__.'/../cache/last-run-warnings-count.txt')) { - $lastRunWarningsCount = (int) file_get_contents(__DIR__.'/../cache/last-run-warnings-count.txt'); - if ($warningCount < $lastRunWarningsCount) { - echo sprintf(' Good job! You fixed %d %s!', $lastRunWarningsCount - $warningCount, $lastRunWarningsCount - $warningCount === 1 ? 'warning' : 'warnings'); - } elseif ($warningCount > $lastRunWarningsCount) { - echo sprintf(' Uh oh! You introduced %d new %s!', $warningCount - $lastRunWarningsCount, $warningCount - $lastRunWarningsCount === 1 ? 'warning' : 'warnings'); + +function displayWarningComparison(int $warningCount): void +{ + $lastRunWarningsFile = __DIR__.'/../cache/last-run-warnings-count.txt'; + + if ($warningCount > 0) { + echo sprintf("\033[33m%s %s found.\033[0m", $warningCount, $warningCount === 1 ? 'warning' : 'warnings'); + if (file_exists($lastRunWarningsFile)) { + $lastRunWarningsCount = (int) file_get_contents($lastRunWarningsFile); + if ($warningCount < $lastRunWarningsCount) { + echo sprintf(' Good job! You fixed %d %s!', $lastRunWarningsCount - $warningCount, $lastRunWarningsCount - $warningCount === 1 ? 'warning' : 'warnings'); + } elseif ($warningCount > $lastRunWarningsCount) { + echo sprintf(' Uh oh! You introduced %d new %s!', $warningCount - $lastRunWarningsCount, $warningCount - $lastRunWarningsCount === 1 ? 'warning' : 'warnings'); + } } } + file_put_contents($lastRunWarningsFile, $warningCount); + echo "\n"; } -file_put_contents(__DIR__.'/../cache/last-run-warnings-count.txt', $warningCount); -echo "\n"; -// If --git flag is passed, make a git commit -if (isset($argv[1]) && $argv[1] === '--git') { - if ($filesChanged > 0) { - echo "\n\033[33mCommitting changes to git...\033[0m\n"; - passthru('git commit -am "Format Markdown"'); - } else { - echo "\n\033[33mNo changes to commit\033[0m\n"; +function commitChanges(): void +{ + global $filesChanged; + + if (isset($argv[1]) && $argv[1] === '--git') { + if ($filesChanged > 0) { + echo "\n\033[33mCommitting changes to git...\033[0m\n"; + passthru('git commit -am "Format Markdown"'); + } else { + echo "\n\033[33mNo changes to commit\033[0m\n"; + } } } + +// Main execution +$dir = __DIR__.'/../../../docs'; +$markdownFiles = find_markdown_files($dir); + +foreach ($markdownFiles as $file) { + handle_file($file); +} + +processHeadings(); +processLinks(); +displayWarnings(); +displaySummary(); +commitChanges(); From 330be78e8aab837f90de5d4c17f6a34c3e287eab Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 14:50:42 +0200 Subject: [PATCH 03/12] Fix wrong page type names --- docs/creating-content/documentation-pages.md | 2 +- docs/creating-content/static-pages.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/creating-content/documentation-pages.md b/docs/creating-content/documentation-pages.md index b0b08f35354..22719ed17d6 100644 --- a/docs/creating-content/documentation-pages.md +++ b/docs/creating-content/documentation-pages.md @@ -24,7 +24,7 @@ saving you time and effort. For more information about this feature, see the [ex ### Best Practices and Hyde Expectations Since Hyde does a lot of things automatically, there are some things you may need -to keep in mind when creating blog posts so that you don't get unexpected results. +to keep in mind when creating documentation pages so that you don't get unexpected results. #### Filenames diff --git a/docs/creating-content/static-pages.md b/docs/creating-content/static-pages.md index ea130f3df7a..8eef4a50093 100644 --- a/docs/creating-content/static-pages.md +++ b/docs/creating-content/static-pages.md @@ -18,7 +18,7 @@ Let's start with the basics. ### Best Practices and Hyde Expectations Since Hyde does a lot of things automatically, there are some things you may need -to keep in mind when creating blog posts so that you don't get unexpected results. +to keep in mind when creating pages so that you don't get unexpected results. #### Filenames From f8b68dfe377ba597d81c1819b4300874fe0812c1 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 15:00:47 +0200 Subject: [PATCH 04/12] Add missing newline to source code --- monorepo/scripts/docs/MarkdownFormatter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/monorepo/scripts/docs/MarkdownFormatter.php b/monorepo/scripts/docs/MarkdownFormatter.php index 3a34c0e70ec..ead9a5a0feb 100644 --- a/monorepo/scripts/docs/MarkdownFormatter.php +++ b/monorepo/scripts/docs/MarkdownFormatter.php @@ -399,6 +399,7 @@ function adjustCaseForSpecialWords(string $text): string return str_ireplace($alwaysLowercase, $alwaysLowercase, $text); } + function fixHeading(string $filename, string $heading, int $headingLevel, string $expectedCase): void { $headingHashes = str_repeat('#', $headingLevel); From ab9de1082d99d03bb5b452778701650cfbf37b8b Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 15:02:36 +0200 Subject: [PATCH 05/12] Update formatter to check and fix headings --- monorepo/scripts/docs/MarkdownFormatter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monorepo/scripts/docs/MarkdownFormatter.php b/monorepo/scripts/docs/MarkdownFormatter.php index ead9a5a0feb..0263e430009 100644 --- a/monorepo/scripts/docs/MarkdownFormatter.php +++ b/monorepo/scripts/docs/MarkdownFormatter.php @@ -18,8 +18,8 @@ $warnings = []; $headings = []; -$checksHeadings = false; -$fixesHeadings = false; +$checksHeadings = true; +$fixesHeadings = true; class MarkdownFormatter { From fa3a7d4951b93ab216c8e910943ae627bc3fb7e5 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 15:09:53 +0200 Subject: [PATCH 06/12] Use APA title case for documentation headings --- monorepo/scripts/docs/MarkdownFormatter.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/monorepo/scripts/docs/MarkdownFormatter.php b/monorepo/scripts/docs/MarkdownFormatter.php index 0263e430009..a36a3c28e4a 100644 --- a/monorepo/scripts/docs/MarkdownFormatter.php +++ b/monorepo/scripts/docs/MarkdownFormatter.php @@ -375,7 +375,12 @@ function processHeadings(): void continue; } - $expectedCase = $headingLevel < 3 ? Hyde\make_title($headingText) : Str::ucfirst($headingText); + // Skip some special cases + if (str_contains($headingText, '"') || str_contains($headingText, '`')) { + continue; + } + + $expectedCase = $headingLevel < 3 ? Str::apa($headingText) : Str::ucfirst($headingText); $expectedCase = adjustCaseForSpecialWords($expectedCase); if ($headingText !== $expectedCase) { @@ -392,8 +397,8 @@ function processHeadings(): void function adjustCaseForSpecialWords(string $text): string { - $alwaysUppercase = ['PHP', 'HTML', 'CLI', 'API', 'YAML', 'XML', 'RSS', 'HydeKernel', 'GitHub']; - $alwaysLowercase = ['to', 'it']; + $alwaysUppercase = ['PHP', 'HTML', 'CLI', 'API', 'YAML', 'XML', 'RSS', 'HydeKernel', 'GitHub', 'CI/CD', 'URL']; + $alwaysLowercase = ['to', 'it', 'and']; $text = str_ireplace($alwaysUppercase, $alwaysUppercase, $text); From 9dbc081fee22fde3812e4d1c2c95c4275208b989 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 15:13:16 +0200 Subject: [PATCH 07/12] Improve special case handling --- monorepo/scripts/docs/MarkdownFormatter.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monorepo/scripts/docs/MarkdownFormatter.php b/monorepo/scripts/docs/MarkdownFormatter.php index a36a3c28e4a..77d6ba333ee 100644 --- a/monorepo/scripts/docs/MarkdownFormatter.php +++ b/monorepo/scripts/docs/MarkdownFormatter.php @@ -375,8 +375,8 @@ function processHeadings(): void continue; } - // Skip some special cases - if (str_contains($headingText, '"') || str_contains($headingText, '`')) { + // Skip some special cases that can't be formatted properly by the APA method + if (Str::contains($headingText, ['"', '`', '-', 'filepath'], true)) { continue; } @@ -397,7 +397,7 @@ function processHeadings(): void function adjustCaseForSpecialWords(string $text): string { - $alwaysUppercase = ['PHP', 'HTML', 'CLI', 'API', 'YAML', 'XML', 'RSS', 'HydeKernel', 'GitHub', 'CI/CD', 'URL']; + $alwaysUppercase = ['PHP', 'HTML', 'CLI', 'API', 'YAML', 'XML', 'RSS', 'HydeKernel', 'HydePage', 'GitHub', 'CI/CD', 'UI ', 'URL']; $alwaysLowercase = ['to', 'it', 'and']; $text = str_ireplace($alwaysUppercase, $alwaysUppercase, $text); From aeb7c5ec6ddf583e680eb79e458903aa9d7aac9a Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 15:15:36 +0200 Subject: [PATCH 08/12] Introduce local variable and change order --- monorepo/scripts/docs/MarkdownFormatter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monorepo/scripts/docs/MarkdownFormatter.php b/monorepo/scripts/docs/MarkdownFormatter.php index 77d6ba333ee..262060322c6 100644 --- a/monorepo/scripts/docs/MarkdownFormatter.php +++ b/monorepo/scripts/docs/MarkdownFormatter.php @@ -400,9 +400,10 @@ function adjustCaseForSpecialWords(string $text): string $alwaysUppercase = ['PHP', 'HTML', 'CLI', 'API', 'YAML', 'XML', 'RSS', 'HydeKernel', 'HydePage', 'GitHub', 'CI/CD', 'UI ', 'URL']; $alwaysLowercase = ['to', 'it', 'and']; + $text = str_ireplace($alwaysLowercase, $alwaysLowercase, $text); $text = str_ireplace($alwaysUppercase, $alwaysUppercase, $text); - return str_ireplace($alwaysLowercase, $alwaysLowercase, $text); + return $text; } function fixHeading(string $filename, string $heading, int $headingLevel, string $expectedCase): void From 8f05945cb72c307ace594a979610122a35f9dc1b Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 15:16:29 +0200 Subject: [PATCH 09/12] Add some extra patches --- monorepo/scripts/docs/MarkdownFormatter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monorepo/scripts/docs/MarkdownFormatter.php b/monorepo/scripts/docs/MarkdownFormatter.php index 262060322c6..a9fd231f7bd 100644 --- a/monorepo/scripts/docs/MarkdownFormatter.php +++ b/monorepo/scripts/docs/MarkdownFormatter.php @@ -403,7 +403,9 @@ function adjustCaseForSpecialWords(string $text): string $text = str_ireplace($alwaysLowercase, $alwaysLowercase, $text); $text = str_ireplace($alwaysUppercase, $alwaysUppercase, $text); - return $text; + $patches = ['items' => 'Items']; + + return strtr($text, $patches); } function fixHeading(string $filename, string $heading, int $headingLevel, string $expectedCase): void From 9e96557476cb7db2c4854b1e88ecb9c8d1ea5341 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 15:16:55 +0200 Subject: [PATCH 10/12] Update documentation to use APA title formatting --- docs/advanced-features/build-tasks.md | 4 ++-- docs/creating-content/compile-and-deploy.md | 2 +- docs/creating-content/documentation-pages.md | 2 +- docs/creating-content/managing-assets.md | 2 +- docs/digging-deeper/customization.md | 2 +- docs/digging-deeper/navigation.md | 2 +- docs/digging-deeper/updating-hyde.md | 4 ++-- docs/getting-started/console-commands.md | 2 +- docs/getting-started/core-concepts.md | 4 ++-- docs/getting-started/front-matter.md | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/advanced-features/build-tasks.md b/docs/advanced-features/build-tasks.md index cb052ce8b6f..8852b58aab1 100644 --- a/docs/advanced-features/build-tasks.md +++ b/docs/advanced-features/build-tasks.md @@ -150,11 +150,11 @@ class MyServiceProvider extends ServiceProvider } ``` -## Error handling +## Error Handling If an exception is thrown in a build task, the build command will catch it and display the error message to the user. -## Helper methods +## Helper Methods ### Output helpers diff --git a/docs/creating-content/compile-and-deploy.md b/docs/creating-content/compile-and-deploy.md index e3699c1dd2e..78e8fcaa014 100644 --- a/docs/creating-content/compile-and-deploy.md +++ b/docs/creating-content/compile-and-deploy.md @@ -4,7 +4,7 @@ navigation: label: "Compile & Deploy" --- -# Compile and Deploy your site +# Compile and Deploy Your Site ## Running the Build Command diff --git a/docs/creating-content/documentation-pages.md b/docs/creating-content/documentation-pages.md index 22719ed17d6..85b1ee52381 100644 --- a/docs/creating-content/documentation-pages.md +++ b/docs/creating-content/documentation-pages.md @@ -167,7 +167,7 @@ For example, putting a Markdown file in `_docs/getting-started/`, is equivalent >info Tip: When using subdirectory-based dropdowns, you can set their priority using the directory name as the array key. -### Hiding items +### Hiding Items You can hide items from the sidebar by adding the `hidden` property to the front matter: diff --git a/docs/creating-content/managing-assets.md b/docs/creating-content/managing-assets.md index fbb97c067ea..a6eba76523e 100644 --- a/docs/creating-content/managing-assets.md +++ b/docs/creating-content/managing-assets.md @@ -53,7 +53,7 @@ It may seem weird to have two folders for storing the compiled assets, but it is The `_site` directory is intended to be excluded from version control, while the `_media` folder is included in the version control. You are of course free to modify this behaviour by editing the `webpack.mix.js` file to change the output directory. -## How Do I Compile assets? +## How Do I Compile Assets? First, make sure that you have installed all the NodeJS dependencies using `npm install`. Then run `npm run dev` to compile the assets. If you want to compile the assets for production, run `npm run prod`. diff --git a/docs/digging-deeper/customization.md b/docs/digging-deeper/customization.md index d835266bdf0..69f96b6041c 100644 --- a/docs/digging-deeper/customization.md +++ b/docs/digging-deeper/customization.md @@ -4,7 +4,7 @@ navigation: priority: 25 --- -# Customizing your Site +# Customizing Your Site ## Introduction diff --git a/docs/digging-deeper/navigation.md b/docs/digging-deeper/navigation.md index 8368204054f..30ada596554 100644 --- a/docs/digging-deeper/navigation.md +++ b/docs/digging-deeper/navigation.md @@ -288,7 +288,7 @@ For example: `_docs/getting-started/installation.md` will be placed in a group c >info Tip: When using subdirectory-based dropdowns, you can set their priority using the directory name as the array key. -## Digging Deeper into the internals +## Digging Deeper Into the Internals While not required to know, you may find it interesting to learn more about how the navigation is handled internally. The best way to learn about this is to look at the source code, so here is a high-level overview with details on where to look in the source code. diff --git a/docs/digging-deeper/updating-hyde.md b/docs/digging-deeper/updating-hyde.md index 612f20f3fde..d957cd1f8dd 100644 --- a/docs/digging-deeper/updating-hyde.md +++ b/docs/digging-deeper/updating-hyde.md @@ -36,7 +36,7 @@ Obligatory related XKCD: [https://xkcd.com/1172](https://xkcd.com/1172) Before you perform an update, please make sure you have a backup of your project. Using Git is highly recommended as it allows you to easily roll back changes if something goes wrong. -## Update to a major version +## Update to a Major Version When updating to a major version, you should read the release notes and the upgrade guide for that version. If you are updating multiple major versions at once, it's recommended to update one major version at a time, @@ -53,7 +53,7 @@ composer update hyde/* --with-dependencies Note that if you have hardcoded a version constraint in your `composer.json` file, you may need to update it manually. You can always refer to the `composer.json` file in the HydePHP repository if you need a reference. -## Alternate update methods +## Alternate Update Methods ### Updating using Git (advanced) diff --git a/docs/getting-started/console-commands.md b/docs/getting-started/console-commands.md index bcdc5552f40..c6e4471c4eb 100644 --- a/docs/getting-started/console-commands.md +++ b/docs/getting-started/console-commands.md @@ -234,7 +234,7 @@ Publish the hyde components for customization. Note that existing files will be |------------|-------------------------| | `category` | The category to publish | -## Display all Registered Routes. +## Display All Registered Routes. diff --git a/docs/getting-started/core-concepts.md b/docs/getting-started/core-concepts.md index 9ebb4d9fd41..99d343fb6bc 100644 --- a/docs/getting-started/core-concepts.md +++ b/docs/getting-started/core-concepts.md @@ -100,7 +100,7 @@ This can be visualized as follows, assuming a blog post is stored as `_posts/hel As you can see, the route key is simply put the relative page URL, without the .html extension. -## Convention over Configuration +## Convention Over Configuration Hyde favours the "Convention over Configuration" paradigm and thus comes preconfigured with sensible defaults. However, Hyde also strives to be modular and endlessly customizable hackable if you need it. @@ -127,7 +127,7 @@ author: "Mr Hyde" date: "2023-03-14" --- -## Markdown comes here +## Markdown Comes Here Lorem ipsum dolor sit amet, etc. ``` diff --git a/docs/getting-started/front-matter.md b/docs/getting-started/front-matter.md index 31e335dfcc0..ea41a9fc581 100644 --- a/docs/getting-started/front-matter.md +++ b/docs/getting-started/front-matter.md @@ -51,7 +51,7 @@ author: website: https://example.com --- -## Markdown comes here +## Markdown Comes Here Lorem ipsum dolor sit amet, etc. ``` From 4f5a7d7b3e00643e4959925e9e2c228255f4b4c0 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 15:36:54 +0200 Subject: [PATCH 11/12] Adjust comparison to better match APA --- monorepo/scripts/docs/MarkdownFormatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monorepo/scripts/docs/MarkdownFormatter.php b/monorepo/scripts/docs/MarkdownFormatter.php index a9fd231f7bd..67d30b951dd 100644 --- a/monorepo/scripts/docs/MarkdownFormatter.php +++ b/monorepo/scripts/docs/MarkdownFormatter.php @@ -384,7 +384,7 @@ function processHeadings(): void $expectedCase = adjustCaseForSpecialWords($expectedCase); if ($headingText !== $expectedCase) { - $caseType = $headingLevel < 3 ? 'title' : 'sentence'; + $caseType = $headingLevel <= 3 ? 'title' : 'sentence'; $warnings['Headings'][] = "Heading '$headingText' should be $caseType case in $filename (expected '$expectedCase')"; if ($fixesHeadings) { From f524200677f8b7e6a45ddac73c62d3b8627ab2d9 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Fri, 12 Jul 2024 15:38:55 +0200 Subject: [PATCH 12/12] Adjust false positive patch to cover singular matches --- monorepo/scripts/docs/MarkdownFormatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monorepo/scripts/docs/MarkdownFormatter.php b/monorepo/scripts/docs/MarkdownFormatter.php index 67d30b951dd..5d211ed7038 100644 --- a/monorepo/scripts/docs/MarkdownFormatter.php +++ b/monorepo/scripts/docs/MarkdownFormatter.php @@ -403,7 +403,7 @@ function adjustCaseForSpecialWords(string $text): string $text = str_ireplace($alwaysLowercase, $alwaysLowercase, $text); $text = str_ireplace($alwaysUppercase, $alwaysUppercase, $text); - $patches = ['items' => 'Items']; + $patches = ['item' => 'Item']; return strtr($text, $patches); }