Skip to content

Commit

Permalink
refactor: W3C markup validation (XHTML to HTML) (#2123)
Browse files Browse the repository at this point in the history
  • Loading branch information
ArnaudLigny authored Feb 24, 2025
1 parent 887953c commit 2a70890
Show file tree
Hide file tree
Showing 16 changed files with 106 additions and 86 deletions.
2 changes: 1 addition & 1 deletion docs/7-Extend.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class MyProcessor extends AbstractPostProcessor
// add a meta tag to the head of the HTML output
if ($format == 'html') {
if (!preg_match('/<meta name="test".*/i', $output)) {
$meta = \sprintf('<meta name="test" content="Test" />');
$meta = \sprintf('<meta name="test" content="Test">');
$output = preg_replace_callback('/([[:blank:]]*)(<\/head>)/i', function ($matches) use ($meta) {
return str_repeat($matches[1] ?: ' ', 2) . $meta . "\n" . $matches[1] . $matches[2];
}, $output);
Expand Down
8 changes: 4 additions & 4 deletions resources/layouts/_default/page.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<html lang="{{ site.language }}">
<head>
{%- block head ~%}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black">
{{- include('partials/metatags.html.twig', {page, site}, with_context = false) ~}}
<style>{% apply minify_css %}
{{- include('partials/new.css.twig') ~}}
Expand Down
2 changes: 1 addition & 1 deletion resources/layouts/_default/page.iframe.twig
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<video id="player" playsinline controls preload="auto"{% if page.poster|default(page.image|default) %} poster="{{ asset(page.poster|default(page.image|default)) }}"{% endif %}>
{%- for video in page.videos ~%}
{%- set asset_video = asset(video) ~%}
<source src="{{ url(asset_video) }}" type="{{ asset_video.subtype }}"/>
<source src="{{ url(asset_video) }}" type="{{ asset_video.subtype }}">
{%- endfor ~%}
</video>
{%- else ~%}
Expand Down
8 changes: 4 additions & 4 deletions resources/layouts/_default/redirect.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<head lang="{{ site.language }}">
<meta charset="utf-8">
<title>{% trans %}Redirecting…{% endtrans %}</title>
<link rel="canonical" href="{{ url(page.redirect, {canonical: true}) }}" />
<meta name="robots" content="noindex" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta http-equiv="refresh" content="0;url={{ url(page.redirect, {canonical: true}) }}" />
<link rel="canonical" href="{{ url(page.redirect, {canonical: true}) }}">
<meta name="robots" content="noindex">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="refresh" content="0;url={{ url(page.redirect, {canonical: true}) }}">
<script>
window.location.assign("{{ url(page.redirect, {canonical: true}) }}");
</script>
Expand Down
4 changes: 2 additions & 2 deletions resources/layouts/partials/alternates-languages.html.twig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{%- if page.translations|default({})|length >= 1 -%}
{%- for alternate in page.translations ~%}
<link rel="alternate" hreflang="{{ alternate.language }}" href="{{ url(alternate, {canonical: true}) }}" />
<link rel="alternate" hreflang="{{ alternate.language }}" href="{{ url(alternate, {canonical: true}) }}">
{%- endfor ~%}
{#- default language ~#}
<link rel="alternate" hreflang="x-default" href="{{ url(page.path, {language: config.language, canonical: true}) }}" />
<link rel="alternate" hreflang="x-default" href="{{ url(page.path, {language: config.language, canonical: true}) }}">
{%- endif -%}
4 changes: 2 additions & 2 deletions resources/layouts/partials/alternates.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
{%- if title is not defined %}{% set title = page.title %}{% endif -%}
{%- for alternate in alternates ~%}
{%- if alternate.rel == 'canonical' and page.canonical.url|default %}{# custom canonical URL ~#}
<link rel="canonical" type="text/html" title="{{ page.canonical.title|default(title|trim) }}" href="{{ url(page.canonical.url) }}" />
<link rel="canonical" type="text/html" title="{{ page.canonical.title|default(title|trim) }}" href="{{ url(page.canonical.url) }}">
{%- else ~%}
<link rel="{{ alternate.rel }}" type="{{ alternate.type }}" title="{{ title|trim }}{% if alternate.format != 'html' %} ({{ alternate.title }}){% endif %}" href="{{ url(page, {canonical: true, format: alternate.format}) }}" />
<link rel="{{ alternate.rel }}" type="{{ alternate.type }}" title="{{ title|trim }}{% if alternate.format != 'html' %} ({{ alternate.title }}){% endif %}" href="{{ url(page, {canonical: true, format: alternate.format}) }}">
{%- endif ~%}
{%- endfor -%}
{%- endif -%}
100 changes: 50 additions & 50 deletions resources/layouts/partials/metatags.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -129,54 +129,54 @@
{%- block content %}
{#~ template ~#}
<title>{% block title %}{{ title_html }}{% endblock %}</title>
<meta name="description" content="{% block description %}{{ description }}{% endblock %}" />
<meta name="description" content="{% block description %}{{ description }}{% endblock %}">
{%- if keywords ~%}
<meta name="keywords" content="{{ keywords|join(', ') }}" />
<meta name="keywords" content="{{ keywords|join(', ') }}">
{%- endif ~%}
{%- if author ~%}
<meta name="author" content="{{ author.name|e }}" />
<meta name="author" content="{{ author.name|e }}">
{%- endif ~%}
<meta name="robots" content="{{ robots }}" />
<meta name="robots" content="{{ robots }}">
{#- template: favicon ~#}
{%- if site.metatags.favicon.enabled ?? true ~%}
{#- favicon.ico ~#}
{%- set favicon_ico = asset('favicon.ico', {'ignore_missing': true}) -%}
{%- if not favicon_ico.missing ~%}
<link rel="icon" href="{{ url(favicon_ico, {'canonical': true}) }}" type="image/x-icon" />
<link rel="shortcut icon" href="{{ url(favicon_ico, {'canonical': true}) }}" type="image/x-icon" />
<link rel="icon" href="{{ url(favicon_ico, {'canonical': true}) }}" type="image/x-icon">
<link rel="shortcut icon" href="{{ url(favicon_ico, {'canonical': true}) }}" type="image/x-icon">
{%- endif -%}
{#- favicon.svg ~#}
{%- set favicon_svg = asset('favicon.svg', {'ignore_missing': true}) -%}
{%- if not favicon_svg.missing ~%}
<link rel="icon" sizes="any" href="{{ url(favicon_svg, {'canonical': true}) }}" type="image/svg+xml" />
<link rel="icon" sizes="any" href="{{ url(favicon_svg, {'canonical': true}) }}" type="image/svg+xml">
{%- endif -%}
{#- favicon.png ~#}
{%- set favicon_asset = asset(site.metatags.favicon.image|default('favicon.png'), {'ignore_missing': true}) -%}
{%- if not favicon_asset.missing -%}
{%- for favicon_variant, favicon_sizes in site.metatags.favicon.sizes|default(favicon_defaults) -%}
{%- for size in favicon_sizes|filter(size => favicon_asset.width >= size) ~%}
<link rel="{{ favicon_variant }}" sizes="{{ size }}x{{ size }}" href="{{ url(favicon_asset|resize(size), {'canonical': true}) }}" type="{{ favicon_asset.subtype }}" />
<link rel="{{ favicon_variant }}" sizes="{{ size }}x{{ size }}" href="{{ url(favicon_asset|resize(size), {'canonical': true}) }}" type="{{ favicon_asset.subtype }}">
{%- endfor -%}
{%- endfor -%}
{%- endif -%}
{%- endif ~%}
{#- template: prev/next ~#}
{%- if page.prev.path is defined ~%}
<link rel="prev" href="{{ url(page.prev, {'canonical': true}) }}" />
<link rel="prev" href="{{ url(page.prev, {'canonical': true}) }}">
{%- endif -%}
{%- if page.next.path is defined ~%}
<link rel="next" href="{{ url(page.next, {'canonical': true}) }}" />
<link rel="next" href="{{ url(page.next, {'canonical': true}) }}">
{%- endif -%}
{#- template: paginator ~#}
{%- if page.paginator.pages is defined ~%}
<link rel="first" href="{{ url(page.paginator.links.first, {'canonical': true}) }}" />
<link rel="first" href="{{ url(page.paginator.links.first, {'canonical': true}) }}">
{%- if page.paginator.links.prev is defined ~%}
<link rel="prev" href="{{ url(page.paginator.links.prev, {'canonical': true}) }}" />
<link rel="prev" href="{{ url(page.paginator.links.prev, {'canonical': true}) }}">
{%- endif ~%}
{%- if page.paginator.links.next is defined ~%}
<link rel="next" href="{{ url(page.paginator.links.next, {'canonical': true}) }}" />
<link rel="next" href="{{ url(page.paginator.links.next, {'canonical': true}) }}">
{%- endif ~%}
<link rel="last" href="{{ url(page.paginator.links.last, {'canonical': true}) }}" />
<link rel="last" href="{{ url(page.paginator.links.last, {'canonical': true}) }}">
{%- endif -%}
{#- template: alternates ~#}
{{- include('partials/alternates.html.twig', {title, page}, with_context = false) ~}}
Expand All @@ -185,73 +185,73 @@
{{- include('partials/alternates-languages.html.twig', {page}, with_context = false) ~}}
{#- template: preload ~#}
{%- if video_asset is defined ~%}
<link rel="preload" href="{{ url(video_asset) }}" as="video" type="{{ video_asset.subtype }}" />
<link rel="preload" href="{{ url(video_asset) }}" as="video" type="{{ video_asset.subtype }}">
{%- endif ~%}
{#- template: rel me ~#}
{%- for social in page.social|default(site.social|default([]))|filter((v) => v['url'] is defined) ~%}
<link rel="me" href="{{ social.url }}" />
<link rel="me" href="{{ social.url }}">
{%- endfor ~%}
{#- template: Open Graph ~#}
<meta property="og:locale" content="{{ opengraph.locale }}" />
<meta property="og:site_name" content="{{ opengraph.site_name }}" />
<meta property="og:type" content="{{ opengraph.type }}" />
<meta property="og:title" content="{{ opengraph.title }}" />
<meta property="og:description" content="{{ opengraph.description }}" />
<meta property="og:url" content="{{ opengraph.url }}" />
<meta property="og:locale" content="{{ opengraph.locale }}">
<meta property="og:site_name" content="{{ opengraph.site_name }}">
<meta property="og:type" content="{{ opengraph.type }}">
<meta property="og:title" content="{{ opengraph.title }}">
<meta property="og:description" content="{{ opengraph.description }}">
<meta property="og:url" content="{{ opengraph.url }}">
{%- if opengraph.image is defined ~%}
<meta property="og:image" content="{{ url(opengraph.image, {'canonical': true}) }}" />
<meta property="og:image:type" content="{{ opengraph.image.subtype }}" />
<meta property="og:image:width" content="{{ opengraph.image.width }}" />
<meta property="og:image:height" content="{{ opengraph.image.height }}" />
<meta property="og:image:alt" content="{{ opengraph.title }}" />
<meta property="og:image" content="{{ url(opengraph.image, {'canonical': true}) }}">
<meta property="og:image:type" content="{{ opengraph.image.subtype }}">
<meta property="og:image:width" content="{{ opengraph.image.width }}">
<meta property="og:image:height" content="{{ opengraph.image.height }}">
<meta property="og:image:alt" content="{{ opengraph.title }}">
{%- endif ~%}
{%- if opengraph.video is defined ~%}
<meta property="og:video" content="{{ url(opengraph.video, {'canonical': true}) }}" />
<meta property="og:video:url" content="{{ url(opengraph.video, {'canonical': true}) }}" />
<meta property="og:video:secure_url" content="{{ url(opengraph.video, {'canonical': true}) }}" />
<meta property="og:video:type" content="{{ opengraph.video.subtype }}" />
<meta property="og:video:width" content="{{ opengraph.video.video.width }}" />
<meta property="og:video:height" content="{{ opengraph.video.video.height }}" />
<meta property="og:video" content="{{ url(opengraph.video, {'canonical': true}) }}">
<meta property="og:video:url" content="{{ url(opengraph.video, {'canonical': true}) }}">
<meta property="og:video:secure_url" content="{{ url(opengraph.video, {'canonical': true}) }}">
<meta property="og:video:type" content="{{ opengraph.video.subtype }}">
<meta property="og:video:width" content="{{ opengraph.video.video.width }}">
<meta property="og:video:height" content="{{ opengraph.video.video.height }}">
{%- endif -%}
{#- template: Facebook ~#}
{%- if facebook.id ~%}
<meta property="fb:profile_id" content="{{ facebook.id }}" />
<meta property="fb:profile_id" content="{{ facebook.id }}">
{%- endif -%}
{%- if facebook.firstname ~%}
<meta property="profile:first_name" content="{{ facebook.firstname }}" />
<meta property="profile:first_name" content="{{ facebook.firstname }}">
{%- endif -%}
{%- if facebook.lastname ~%}
<meta property="profile:last_name" content="{{ facebook.lastname }}" />
<meta property="profile:last_name" content="{{ facebook.lastname }}">
{%- endif -%}
{%- if facebook.username ~%}
<meta property="profile:username" content="{{ facebook.username }}" />
<meta property="profile:username" content="{{ facebook.username }}">
{%- endif ~%}
{#- template: Twitter ~#}
<meta name="twitter:title" content="{{ opengraph.title }}" />
<meta name="twitter:description" content="{{ opengraph.description }}" />
<meta name="twitter:title" content="{{ opengraph.title }}">
<meta name="twitter:description" content="{{ opengraph.description }}">
{%- if opengraph.image is defined and opengraph.image.width > 500 ~%}
<meta name="twitter:image" content="{{ opengraph.image|url({'canonical': true}) }}" />
<meta name="twitter:image:alt" content="{{ opengraph.title }}" />
<meta name="twitter:image" content="{{ opengraph.image|url({'canonical': true}) }}">
<meta name="twitter:image:alt" content="{{ opengraph.title }}">
{%- if opengraph.video is defined ~%}
<meta name="twitter:card" content="player" />
<meta name="twitter:player" content="{{ url(page, {'canonical': true, 'format': 'iframe'}) }}" />
<meta name="twitter:player:width" content="{{ opengraph.video.video.width }}" />
<meta name="twitter:player:height" content="{{ opengraph.video.video.height }}" />
<meta name="twitter:card" content="player">
<meta name="twitter:player" content="{{ url(page, {'canonical': true, 'format': 'iframe'}) }}">
<meta name="twitter:player:width" content="{{ opengraph.video.video.width }}">
<meta name="twitter:player:height" content="{{ opengraph.video.video.height }}">
{%- else ~%}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:card" content="summary_large_image">
{%- endif ~%}
{%- else ~%}
<meta name="twitter:card" content="summary" />
<meta name="twitter:card" content="summary">
{%- endif -%}
{%- if twitter.site ~%}
<meta name="twitter:site" content="@{{ twitter.site }}" />
<meta name="twitter:site" content="@{{ twitter.site }}">
{%- endif -%}
{%- if twitter.creator ~%}
<meta name="twitter:creator" content="@{{ twitter.creator }}" />
<meta name="twitter:creator" content="@{{ twitter.creator }}">
{%- endif ~%}
{#- template: Mastodon ~#}
{%- if mastodon.creator ~%}
<meta name="fediverse:creator" content="{{ mastodon.creator }}" />
<meta name="fediverse:creator" content="{{ mastodon.creator }}">
{%- endif ~%}
{#- template: json-ld ~#}
{{- include('partials/jsonld.js.twig', {author, favicon_asset: favicon_asset|default}) ~}}
Expand Down
20 changes: 20 additions & 0 deletions src/Converter/Parsedown.php
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,26 @@ protected function parseAttributeData($attributeString)
return $Data;
}

/**
* {@inheritdoc}
*
* Converts XHTML '<br />' tag to '<br>'.
*/
protected function unmarkedText($text)
{
return str_replace("<br />", "<br>", parent::unmarkedText($text)); // @phpstan-ignore staticMethod.notFound
}

/**
* {@inheritdoc}
*
* XHTML closing tag to HTML5 closing tag.
*/
protected function element(array $Element)
{
return str_replace(" />", ">", parent::element($Element)); // @phpstan-ignore staticMethod.notFound
}

/**
* Turns a path relative to static or assets into a website relative path.
*
Expand Down
4 changes: 2 additions & 2 deletions src/Renderer/Extension/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -642,8 +642,8 @@ public function excerpt(?string $string, int $length = 450, string $suffix = '
{
$string = $string ?? '';

$string = str_replace('</p>', '<br /><br />', $string);
$string = trim(strip_tags($string, '<br>'), '<br />');
$string = str_replace('</p>', '<br><br>', $string);
$string = trim(strip_tags($string, '<br>'));
if (mb_strlen($string) > $length) {
$string = mb_substr($string, 0, $length);
$string .= $suffix;
Expand Down
2 changes: 1 addition & 1 deletion src/Renderer/PostProcessor/GeneratorMetaTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function process(Page $page, string $output, string $format): string
{
if ($format == 'html') {
if (!preg_match('/<meta name="generator".*/i', $output)) {
$meta = \sprintf('<meta name="generator" content="Cecil %s" />', Builder::getVersion());
$meta = \sprintf('<meta name="generator" content="Cecil %s">', Builder::getVersion());
$output = preg_replace_callback('/([[:blank:]]*)(<\/head>)/i', function ($matches) use ($meta) {
return str_repeat($matches[1] ?: ' ', 2) . $meta . "\n" . $matches[1] . $matches[2];
}, $output);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Test extends AbstractPostProcessor
public function process(Page $page, string $output, string $format): string
{
if ($format == 'html') {
$test = \sprintf('<meta name="test" content="TEST" />');
$test = \sprintf('<meta name="test" content="TEST">');
$output = preg_replace_callback('/([[:blank:]]*)(<\/head>)/i', function ($matches) use ($test) {
return str_repeat($matches[1] ?: ' ', 2) . $test . "\n" . $matches[1] . $matches[2];
}, $output);
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/website/layouts/assets.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@
{{- asset('cecil-logo-1000.png')|lqip -}}
</blockquote>
<blockquote>
<img src="{{ asset('cecil-logo-1000.png')|lqip }}" />
<img src="{{ asset('cecil-logo-1000.png')|lqip }}">
</blockquote>
</p>
<h3>Read EXIF data</h3>
Expand Down
22 changes: 11 additions & 11 deletions tests/fixtures/website/layouts/intl.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
<div class="post-date">{% trans %}Publication date:{% endtrans %} {{ page.date|format_date('long', locale='en') }} (format_date, locale=en)</div>
{% endif %}
{% if page.image is defined %}
<img src="{{ page.image }}" />
<img src="{{ page.image }}">
{% endif %}
{{ page.content }}
<hr />
<hr>
<h2>{% trans %}Translation examples{% endtrans %}</h2>
<ul>
<li>{% trans %}Simple text{% endtrans %}</li>
Expand All @@ -35,25 +35,25 @@
{% trans with {'%count%': 42}%}{0}I don't have apples|{1}I have one apple|]1,Inf[I have %count% apples{% endtrans %}
</li>
</ul>
<hr />
<hr>
<h2>{% trans %}Page's language{% endtrans %}</h2>
<p>
- code: {{ site.language }}<br />
- name: {{ site.language.name }} ({{ site.language.locale|language_name|capitalize }})<br />
- locale: {{ site.language.locale }}<br />
- weight: {{ site.language.weight }}<br />
- code: {{ site.language }}<br>
- name: {{ site.language.name }} ({{ site.language.locale|language_name|capitalize }})<br>
- locale: {{ site.language.locale }}<br>
- weight: {{ site.language.weight }}<br>
</p>
<p>
{% trans %}Default language:{% endtrans %} {{ site.languages.0.name }} ({{ site.languages.0.locale|language_name|capitalize }})<br />
{% trans %}Default language:{% endtrans %} {{ site.languages.0.name }} ({{ site.languages.0.locale|language_name|capitalize }})<br>
</p>
<hr />
<hr>
<h2>{% trans %}Available translations:{% endtrans %}</h2>
<p>
{% for p in site.allpages|filter_by('langref', page.langref) %}
- <a href="{{ url(p) }}">{{ p.title }}</a><br />
- <a href="{{ url(p) }}">{{ p.title }}</a><br>
{% endfor %}
</p>
<hr />
<hr>
<h2>site.page</h2>
<p>
<pre>site.page('about').title</pre>
Expand Down
Loading

0 comments on commit 2a70890

Please sign in to comment.