Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions benchmarks/OutputMinifierBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace YiiPress\Benchmarks;

use YiiPress\Build\OutputMinifier;
use PhpBench\Attributes\Iterations;
use PhpBench\Attributes\Revs;
use PhpBench\Attributes\Warmup;

final class OutputMinifierBench
{
private string $html;

public function __construct()
{
$block = <<<'HTML'
<article>
<h2>Generated output</h2>
<p>YiiPress keeps generated pages compact while preserving code.</p>
<pre><code>Line 1
Line 2</code></pre>
<script>
const message = " keep script spacing ";
</script>
</article>
HTML;

$this->html = str_repeat($block, 100);
}

#[Revs(100)]
#[Iterations(3)]
#[Warmup(1)]
public function benchHtmlMinification(): void
{
OutputMinifier::html($this->html);
}
}
14 changes: 14 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ params:
assets:
fingerprint: true

minify: true

last_updated: true
edit_page: https://github.com/example/mysite/edit/main/content/{path}
report_issue: https://github.com/example/mysite/issues/new?title=Docs:%20{title}&body={url}
Expand Down Expand Up @@ -72,6 +74,7 @@ editor: code
- **toc** — generate a table of contents from headings (default: `true`); set to `false` to disable globally. When enabled, heading tags receive `id` attributes and a `$toc` variable is passed to templates
- **search** — opt-in client-side search (see below)
- **related** — opt-in related content suggestions (see below)
- **minify** — minify generated HTML output (default: `true`); set to `false` to keep rendered template whitespace
- **last_updated** — set to `true` to show each entry source file's last modification time below its content (default: `false`)
- **edit_page** — URL template for an optional "Edit this page" link below entry content (see below)
- **report_issue** — URL template for an optional "Report an issue" link below entry content (see below)
Expand Down Expand Up @@ -211,6 +214,17 @@ asset references in rendered HTML are rewritten during build so custom themes co
Root-relative local asset references are treated as YiiPress site-root paths and emitted relative to
the current output page, so they remain valid when `base_url` contains a deployment path.

### Output minification

Generated HTML pages are minified by default:

```yaml
minify: true
```

Set `minify: false` to keep template indentation and line breaks in generated `*.html` files.
Whitespace inside `pre`, `textarea`, `script`, and `style` elements is preserved either way.

### Editor

During `yiipress serve`, HTML pages get a fixed bottom-right **Edit** button. Clicking it asks the preview server to open the markdown source file that produced the current page.
Expand Down
1 change: 1 addition & 0 deletions roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
- [x] Static file copying (fonts, downloads, PDFs from source to output)
- [x] Root-relative asset URLs stay valid when deploying under a subdirectory
- [x] Nightly Linux binary published for GitHub Actions preview builds
- [x] Configurable generated HTML output minification

## Priority 6: SEO and web standards

Expand Down
16 changes: 13 additions & 3 deletions src/Build/AuthorPageWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function write(
return 0;
}

$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest);
$renderer = $this->createPageTemplateRenderer($siteConfig);

$this->writeIndex($siteConfig, $authors, $outputDir, $navigation, $noWrite);
$pageCount = 1;
Expand All @@ -65,7 +65,7 @@ public function writeIndex(
bool $noWrite = false,
): void {
$this->writeIndexPage(
new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest),
$this->createPageTemplateRenderer($siteConfig),
$siteConfig,
$authors,
$outputDir,
Expand All @@ -89,7 +89,7 @@ public function writeAuthor(
bool $noWrite = false,
): void {
$this->writeAuthorPage(
$renderer ?? new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest),
$renderer ?? $this->createPageTemplateRenderer($siteConfig),
$siteConfig,
$author,
$entries,
Expand All @@ -100,6 +100,16 @@ public function writeAuthor(
);
}

private function createPageTemplateRenderer(SiteConfig $siteConfig): PageTemplateRenderer
{
return new PageTemplateRenderer(
$this->templateResolver,
$siteConfig->theme,
$this->assetManifest,
$siteConfig->minify,
);
}

/**
* @param array<string, Author> $authors
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Build/CollectionListingWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function write(
int $workerCount = 1,
bool $noWrite = false,
): int {
$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest);
$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest, $siteConfig->minify);
$perPage = $collection->entriesPerPage;
if ($perPage <= 0) {
$perPage = count($entries) ?: 1;
Expand Down
2 changes: 1 addition & 1 deletion src/Build/DateArchiveWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function write(
int $workerCount = 1,
bool $noWrite = false,
): int {
$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest);
$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest, $siteConfig->minify);
$byYear = [];
$byMonth = [];

Expand Down
8 changes: 6 additions & 2 deletions src/Build/EntryRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ public function render(
$cacheContext = $this->cacheContext($siteConfig, $entry, $permalink, $navigation, $crossRefResolver, $navigationPager);
$cached = $this->cache->get($entry->filePath, $cacheContext);
if ($cached !== null) {
return $this->dispatchRenderFinished($siteConfig, $entry, $permalink, $cached);
$html = $this->dispatchRenderFinished($siteConfig, $entry, $permalink, $cached);

return $siteConfig->minify ? OutputMinifier::html($html) : $html;
}
}

Expand All @@ -91,7 +93,9 @@ public function render(
$this->cache->set($entry->filePath, $html, $cacheContext);
}

return $this->dispatchRenderFinished($siteConfig, $entry, $permalink, $html);
$html = $this->dispatchRenderFinished($siteConfig, $entry, $permalink, $html);

return $siteConfig->minify ? OutputMinifier::html($html) : $html;
}

private function dispatchRenderFinished(SiteConfig $siteConfig, Entry $entry, string $permalink, string $html): string
Expand Down
2 changes: 1 addition & 1 deletion src/Build/NotFoundPageWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function write(SiteConfig $siteConfig, string $outputDir, ?Navigation $na
{
$rootPath = './';
$uiViewData = UiViewData::forSite($siteConfig, $this->templateResolver, $siteConfig->theme);
$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest);
$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest, $siteConfig->minify);
$html = $renderer->render('errors/404', [
'siteTitle' => $siteConfig->title,
'nav' => $navigation,
Expand Down
128 changes: 128 additions & 0 deletions src/Build/OutputMinifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

declare(strict_types=1);

namespace YiiPress\Build;

use function preg_replace;
use function preg_split;
use function preg_match_all;
use function strlen;
use function str_starts_with;
use function substr;
use function trim;

final class OutputMinifier
{
private const string PROTECTED_ELEMENT_PATTERN = '~<(?<tag>pre|textarea|script|style)\b(?:[^>"\']+|"[^"]*"|\'[^\']*\')*>.*?</\k<tag>>~is';
private const string TAG_PATTERN = '~(<(?:[^>"\']+|"[^"]*"|\'[^\']*\')*>)~';

/**
* Minifies generated HTML while preserving whitespace-sensitive element bodies.
*/
public static function html(string $html): string
{
if ($html === '') {
return '';
}

$protectedParts = self::splitProtectedParts($html);
if ($protectedParts === null) {
return $html;
}

$minified = '';
$previousPartProtected = false;
foreach ($protectedParts as $part) {
if ($part['html'] === '') {
continue;
}

if ($part['protected']) {
$minified = preg_replace('~(?<=>)\s+$~', '', $minified) ?? $minified;
$minified .= $part['html'];
$previousPartProtected = true;
continue;
}

$fragment = self::minifyHtmlFragment($part['html']);
if ($previousPartProtected) {
$fragment = preg_replace('~^\s+(?=<)~', '', $fragment) ?? $fragment;
}

$minified .= $fragment;
$previousPartProtected = false;
}

return trim($minified);
}

/**
* @return list<array{html: string, protected: bool}>|null
*/
private static function splitProtectedParts(string $html): ?array
{
$matched = preg_match_all(self::PROTECTED_ELEMENT_PATTERN, $html, $matches, PREG_OFFSET_CAPTURE);
if ($matched === false) {
return null;
}

if ($matched === 0) {
return [['html' => $html, 'protected' => false]];
}

$parts = [];
$offset = 0;
foreach ($matches[0] as [$match, $position]) {
if ($position > $offset) {
$parts[] = [
'html' => substr($html, $offset, $position - $offset),
'protected' => false,
];
}

$parts[] = [
'html' => $match,
'protected' => true,
];
$offset = $position + strlen($match);
}

if ($offset < strlen($html)) {
$parts[] = [
'html' => substr($html, $offset),
'protected' => false,
];
}

return $parts;
}

private static function minifyHtmlFragment(string $html): string
{
if (trim($html) === '') {
return '';
}

$tokens = preg_split(self::TAG_PATTERN, $html, -1, PREG_SPLIT_DELIM_CAPTURE);
if ($tokens === false) {
return $html;
}

$minified = '';
foreach ($tokens as $token) {
if ($token === '') {
continue;
}

if (str_starts_with($token, '<')) {
$minified .= $token;
continue;
}

$minified .= preg_replace('~[ \t\r\n\f]+~', ' ', $token) ?? $token;
}

return preg_replace('~>\s+<~', '><', $minified) ?? $minified;
}
}
5 changes: 4 additions & 1 deletion src/Build/PageTemplateRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function __construct(
private readonly TemplateResolver $templateResolver,
private readonly string $themeName,
private readonly ?AssetFingerprintManifest $assetManifest = null,
private readonly bool $minify = true,
) {}

/**
Expand Down Expand Up @@ -54,6 +55,8 @@ public function render(string $templateName, array $variables, string $rootPath)

$html = ($this->templateClosures[$templatePath])($variables);

return $this->templateContexts[$this->themeName]->rewriteHtml($html, $rootPath);
$html = $this->templateContexts[$this->themeName]->rewriteHtml($html, $rootPath);

return $this->minify ? OutputMinifier::html($html) : $html;
}
}
4 changes: 4 additions & 0 deletions src/Build/RedirectPageWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public function write(
</html>
HTML;

if ($siteConfig?->minify ?? true) {
$html = OutputMinifier::html($html);
}

if ($noWrite) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Build/TaxonomyPageWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function write(
?Navigation $navigation = null,
bool $noWrite = false,
): int {
$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest);
$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest, $siteConfig->minify);
$pageCount = 0;

foreach ($taxonomyData as $taxonomyName => $terms) {
Expand Down
1 change: 1 addition & 0 deletions src/Content/Model/SiteConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ public function __construct(
public ?string $editPageUrl = null,
public ?string $reportIssueUrl = null,
public bool $authorPages = false,
public bool $minify = true,
) {}
}
1 change: 1 addition & 0 deletions src/Content/Parser/SiteConfigParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public function parse(string $filePath): SiteConfig
editPageUrl: self::parseOptionalString($data['edit_page'] ?? null),
reportIssueUrl: self::parseOptionalString($data['report_issue'] ?? null),
authorPages: (bool) ($data['author_pages'] ?? false),
minify: (bool) ($data['minify'] ?? true),
);
}

Expand Down
2 changes: 2 additions & 0 deletions tests/Unit/Build/EntryRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ private function createSiteConfig(
?string $editPageUrl = null,
?string $reportIssueUrl = null,
bool $authorPages = false,
bool $minify = true,
): SiteConfig
{
return new SiteConfig(
Expand All @@ -573,6 +574,7 @@ private function createSiteConfig(
editPageUrl: $editPageUrl,
reportIssueUrl: $reportIssueUrl,
authorPages: $authorPages,
minify: $minify,
);
}

Expand Down
Loading
Loading