pandorga 1.6.3 → 1.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +11 -0
- data/_includes/content-runtime/60-fragments.html +3 -2
- data/_includes/home/band-listing.html +10 -3
- data/_includes/theme/font-loader.html +2 -1
- data/_layouts/home.html +8 -3
- data/_layouts/shell.html +19 -5
- data/_plugins/content_headers.rb +12 -0
- data/assets/css/base.css +31 -1
- data/assets/css/ledger/home.css +13 -0
- data/lib/pandorga/jekyll/plugins/content_headers.rb +12 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/lighthouse-home.sh +99 -0
- data/scripts/lighthouse_server.py +74 -0
- data/scripts/test/test-lighthouse-policy.rb +79 -0
- data/scripts/validate.sh +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b772584dba676ef335aa1f56a912948ce50d9a142334711a24e9ebf6629f35e
|
|
4
|
+
data.tar.gz: 046a693ae7e9cc93e4c300e998aeea7f72c0b31cfd9fa98847ed4a05c6761964
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 705f9a05d328407ea488e5493642debaaad9ee4775a7ac1fc0587adf494065960fb5f2f6b202c3ff14c04d21e7812cb02abef1f39b8a2c85da7bfe2a96e941c3
|
|
7
|
+
data.tar.gz: ebd39b4db3858646577c254a590b6a5925d6b90e62b04d41bd29c17d13d9b3dfeeb2a8ced0189c227520d532ddaa3dc1419888dfd02f37c53802d99c7565c1a2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.5] — 2026-10-10
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- The example home measured like the public site: compressed responses,
|
|
8
|
+
served concurrently. The stock one-thread HTTP/1.0 server was scoring
|
|
9
|
+
the same templates in the 70s while the live site on 1.6.3 scored 97.
|
|
10
|
+
- The Material Symbols face is about a megabyte. It is no longer linked
|
|
11
|
+
from the head. The shell adds that sheet after the load event, so the
|
|
12
|
+
simulator does not wait on it for the largest paint. The text font
|
|
13
|
+
sheet and the flag stylesheet use print media and apply on load.
|
|
14
|
+
- The icon box stays 1em.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Lighthouse mobile thresholds apply to releases. Pull requests and merges
|
|
19
|
+
to main only warn. The release build checks out the tag and fails below 75.
|
|
20
|
+
|
|
21
|
+
No `pandorga:` config keys were added or renamed.
|
|
22
|
+
|
|
23
|
+
## [1.6.4] — 2026-10-10
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- The Material Symbols stylesheet no longer blocks first paint. It loads
|
|
28
|
+
as print media and is applied on load. The icon box stays 1em, and its
|
|
29
|
+
size stays inherited, so the Google sheet cannot restyle it after paint.
|
|
30
|
+
- The content runtime, including marked, comes after the page so the
|
|
31
|
+
hero is in the document before that script. A home band's kicker, title,
|
|
32
|
+
and note from `content/pages/headers.yml` are in the HTML.
|
|
33
|
+
- The hero subtitle from `content/pages/headers.yml` is in the HTML.
|
|
34
|
+
Leaving that line empty until script runs moved the portrait.
|
|
35
|
+
- On a phone the hero band is shorter, so the first section title stays in
|
|
36
|
+
the first viewport. That title was the late LCP element.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- Lighthouse mobile is a release rule. A stable release scores 90 or above.
|
|
41
|
+
A score from 75 to 89 is acceptable only to ship a bug fix, and an urgent
|
|
42
|
+
performance campaign must then bring the score back to 90 or above.
|
|
43
|
+
Below 75 is never acceptable. CI runs one mobile pass on the example home.
|
|
44
|
+
|
|
45
|
+
No `pandorga:` config keys were added or renamed.
|
|
46
|
+
|
|
3
47
|
## [1.6.3] — 2026-10-10
|
|
4
48
|
|
|
5
49
|
### Fixed
|
data/README.md
CHANGED
|
@@ -75,6 +75,17 @@ bundle exec pandorga serve
|
|
|
75
75
|
| [docs/deploy/cloudflare.md](docs/deploy/cloudflare.md) | Pages + object store |
|
|
76
76
|
| [docs/spec.md](docs/spec.md) | Migration plan and acceptance criteria |
|
|
77
77
|
| [docs/decisions.md](docs/decisions.md) | Frozen decisions |
|
|
78
|
+
| [CONTRIBUTING.md](CONTRIBUTING.md) | Gates, and the Lighthouse mobile release rule |
|
|
79
|
+
|
|
80
|
+
## Releases
|
|
81
|
+
|
|
82
|
+
Lighthouse mobile thresholds apply to releases. A stable release scores
|
|
83
|
+
90 or above. A score from 75 to 89 is acceptable only to ship a bug fix,
|
|
84
|
+
and an urgent performance campaign must then bring it back to 90 or above.
|
|
85
|
+
Below 75 is never acceptable for a release. A pull request or a merge may
|
|
86
|
+
score lower when that is unavoidable to keep the work modular. The release
|
|
87
|
+
build fails below 75. Pull requests and merges only warn. The checklist
|
|
88
|
+
is in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
78
89
|
|
|
79
90
|
## License
|
|
80
91
|
|
|
@@ -329,8 +329,9 @@
|
|
|
329
329
|
return;
|
|
330
330
|
}
|
|
331
331
|
if (inline) {
|
|
332
|
-
|
|
333
|
-
|
|
332
|
+
var next = stripSingleParagraph(parseInlineMarkdown(text));
|
|
333
|
+
if (el.innerHTML.trim() !== next.trim()) el.innerHTML = next;
|
|
334
|
+
} else if (el.textContent.trim() !== text) {
|
|
334
335
|
el.textContent = text;
|
|
335
336
|
}
|
|
336
337
|
el.hidden = false;
|
|
@@ -23,11 +23,18 @@
|
|
|
23
23
|
data-home-language="{{ _lang }}"
|
|
24
24
|
>
|
|
25
25
|
<div class="home-band-inner">
|
|
26
|
+
{%- assign _section = site.data.page_headers[band.section_key] -%}
|
|
27
|
+
{%- assign _title = band.title -%}
|
|
28
|
+
{%- if _section.title != blank -%}{%- assign _title = _section.title -%}{%- endif -%}
|
|
26
29
|
<div class="ledger-section-banner" data-content-home-section="{{ band.section_key }}">
|
|
27
30
|
<div class="ledger-section-lead">
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
+
{%- if _section.kicker != blank -%}
|
|
32
|
+
<span class="ledger-kicker" data-content-section-kicker>{{ _section.kicker }}</span>
|
|
33
|
+
{%- endif -%}
|
|
34
|
+
<h2 class="section-title" data-content-section-title>{{ _title }}</h2>
|
|
35
|
+
{%- if _section.note != blank -%}
|
|
36
|
+
<p class="home-section-note font-body" data-content-section-note>{{ _section.note }}</p>
|
|
37
|
+
{%- endif -%}
|
|
31
38
|
</div>
|
|
32
39
|
<div class="home-section-aside">
|
|
33
40
|
<a class="ledger-cta" href="{{ _path }}">
|
|
@@ -52,4 +52,5 @@
|
|
|
52
52
|
{% endif %}
|
|
53
53
|
{% assign fonts_href = fonts_href | append: "&display=swap" %}
|
|
54
54
|
<link rel="preload" as="style" href="{{ fonts_href }}" />
|
|
55
|
-
<link href="{{ fonts_href }}"
|
|
55
|
+
<link rel="stylesheet" href="{{ fonts_href }}" media="print" onload="this.media='all'" />
|
|
56
|
+
<noscript><link rel="stylesheet" href="{{ fonts_href }}" /></noscript>
|
data/_layouts/home.html
CHANGED
|
@@ -9,9 +9,9 @@ layout: shell
|
|
|
9
9
|
plus content fragments. Decorative art is a slot
|
|
10
10
|
(`pandorga.home.hero_art`, default page/hero-default.html). Listing and
|
|
11
11
|
index bands come from site.data.pandorga_home_bands in registry order.
|
|
12
|
-
Band copy (kicker, title, note)
|
|
13
|
-
|
|
14
|
-
in this layout.
|
|
12
|
+
Band copy (kicker, title, note) and the hero subtitle come from
|
|
13
|
+
content/pages/headers.yml at build time. No Portuguese or owner-specific
|
|
14
|
+
strings live in this layout.
|
|
15
15
|
{%- endcomment -%}
|
|
16
16
|
|
|
17
17
|
{%- assign _hero_name = site.pandorga.identity | pandorga_hero_name: site.author, site.title -%}
|
|
@@ -28,8 +28,13 @@ layout: shell
|
|
|
28
28
|
<div class="home-band-inner">
|
|
29
29
|
<div class="home-hero">
|
|
30
30
|
<div class="home-hero-copy">
|
|
31
|
+
{%- assign _home_header = site.data.page_headers.home -%}
|
|
32
|
+
{%- if _home_header.subtitle != blank -%}
|
|
33
|
+
<div id="home-subtitle-content" class="home-hero-tagline font-label" data-content-static>{{ _home_header.subtitle }}</div>
|
|
34
|
+
{%- else -%}
|
|
31
35
|
<div id="home-subtitle-content" class="home-hero-tagline font-label"
|
|
32
36
|
data-content-fragment="home/subtitle" data-content-quiet></div>
|
|
37
|
+
{%- endif -%}
|
|
33
38
|
<h1 class="home-hero-title">
|
|
34
39
|
<span class="home-hero-given">{{ _hero_name.given }}</span>{%- if _hero_name.family != "" %} <span class="home-hero-family">{{ _hero_name.family }}</span>{%- endif %}
|
|
35
40
|
</h1>
|
data/_layouts/shell.html
CHANGED
|
@@ -116,9 +116,23 @@
|
|
|
116
116
|
<link rel="stylesheet" href="/assets/css/ledger/{{ _sheet }}.css" />
|
|
117
117
|
{%- endfor -%}
|
|
118
118
|
{% include theme/font-loader.html %}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
{%- comment -%}
|
|
120
|
+
Icons are not first paint. The face is about a megabyte. A link in the
|
|
121
|
+
head, even with print media, is discovered immediately and the simulator
|
|
122
|
+
then waits on that face for the largest paint. Add the sheet after load.
|
|
123
|
+
{%- endcomment -%}
|
|
124
|
+
{%- assign _symbols_href = "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" -%}
|
|
125
|
+
<script>
|
|
126
|
+
window.addEventListener('load', function () {
|
|
127
|
+
var link = document.createElement('link');
|
|
128
|
+
link.rel = 'stylesheet';
|
|
129
|
+
link.href = {{ _symbols_href | jsonify }};
|
|
130
|
+
document.head.appendChild(link);
|
|
131
|
+
});
|
|
132
|
+
</script>
|
|
133
|
+
<noscript><link rel="stylesheet" href="{{ _symbols_href }}" /></noscript>
|
|
134
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" media="print" onload="this.media='all'" />
|
|
135
|
+
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" /></noscript>
|
|
122
136
|
<style>
|
|
123
137
|
:root { --drop-cap-lines: {{ site.drop_cap_lines | default: 2 }}; }
|
|
124
138
|
</style>
|
|
@@ -233,8 +247,6 @@
|
|
|
233
247
|
})();
|
|
234
248
|
</script>
|
|
235
249
|
|
|
236
|
-
{% include page/content-runtime.html %}
|
|
237
|
-
|
|
238
250
|
<div id="page-content" tabindex="-1">
|
|
239
251
|
{{ content }}
|
|
240
252
|
</div>
|
|
@@ -244,6 +256,8 @@
|
|
|
244
256
|
<!-- Footer -->
|
|
245
257
|
{% include page/site-footer.html nav_active=nav_active %}
|
|
246
258
|
|
|
259
|
+
{% include page/content-runtime.html %}
|
|
260
|
+
|
|
247
261
|
</body>
|
|
248
262
|
|
|
249
263
|
</html>
|
data/_plugins/content_headers.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Jekyll
|
|
|
25
25
|
site.data["content_fragments"] ||= {}
|
|
26
26
|
|
|
27
27
|
load_page_content(site, content_path)
|
|
28
|
+
load_page_headers(site, content_path)
|
|
28
29
|
load_structured_data(site, content_path)
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -40,6 +41,17 @@ module Jekyll
|
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
# content/pages/headers.yml is also exported as pages/headers.json.
|
|
45
|
+
# The home bands read it here so a section title and note are in the
|
|
46
|
+
# HTML. A heading painted later by the runtime becomes the LCP element.
|
|
47
|
+
def load_page_headers(site, content_path)
|
|
48
|
+
path = File.join(content_path, "pages", "headers.yml")
|
|
49
|
+
return unless File.file?(path)
|
|
50
|
+
|
|
51
|
+
loaded = load_yaml(path)
|
|
52
|
+
site.data["page_headers"] = loaded.is_a?(Hash) ? loaded : {}
|
|
53
|
+
end
|
|
54
|
+
|
|
43
55
|
def load_structured_data(site, content_path)
|
|
44
56
|
data_path = File.join(content_path, "collections", "data")
|
|
45
57
|
return unless Dir.exist?(data_path)
|
data/assets/css/base.css
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
/* ── Material Symbols ── */
|
|
2
|
-
|
|
2
|
+
/* Doubled class so this beats the Google stylesheet, which arrives later
|
|
3
|
+
and sets font-size: 24px. That late size change moves the hero. */
|
|
4
|
+
.material-symbols-outlined.material-symbols-outlined {
|
|
3
5
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
|
6
|
+
font-weight: normal;
|
|
7
|
+
font-style: normal;
|
|
8
|
+
font-size: inherit;
|
|
9
|
+
letter-spacing: normal;
|
|
4
10
|
display: inline-flex;
|
|
5
11
|
align-items: center;
|
|
6
12
|
justify-content: center;
|
|
7
13
|
vertical-align: middle;
|
|
8
14
|
line-height: 1;
|
|
9
15
|
text-transform: none;
|
|
16
|
+
white-space: nowrap;
|
|
17
|
+
/* One em square. Until the icon face arrives the ligature is a word,
|
|
18
|
+
and an unsized word widens the control. */
|
|
19
|
+
width: 1em;
|
|
20
|
+
height: 1em;
|
|
21
|
+
min-width: 1em;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
flex-shrink: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.material-symbols-outlined.text-sm { font-size: 0.875rem; }
|
|
27
|
+
.material-symbols-outlined.text-lg { font-size: 1.125rem; }
|
|
28
|
+
.material-symbols-outlined.text-xl { font-size: 1.25rem; }
|
|
29
|
+
.material-symbols-outlined.text-5xl { font-size: 3rem; }
|
|
30
|
+
|
|
31
|
+
@media (min-width: 768px) {
|
|
32
|
+
.material-symbols-outlined.text-6xl { font-size: 3.75rem; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ledger-cta-arrow.material-symbols-outlined,
|
|
36
|
+
.ledger-cta-icon.material-symbols-outlined {
|
|
37
|
+
font-size: 1.35em;
|
|
10
38
|
}
|
|
11
39
|
|
|
12
40
|
/* Cubist triad — ~+25% over the 1.25rem theme-toggle box (rounded to 1.5rem). */
|
|
@@ -22,6 +50,8 @@
|
|
|
22
50
|
font-size: var(--type-card);
|
|
23
51
|
width: 1.25rem;
|
|
24
52
|
height: 1.25rem;
|
|
53
|
+
min-width: 1.25rem;
|
|
54
|
+
max-width: 1.25rem;
|
|
25
55
|
line-height: 1;
|
|
26
56
|
}
|
|
27
57
|
|
data/assets/css/ledger/home.css
CHANGED
|
@@ -74,6 +74,19 @@
|
|
|
74
74
|
overflow: hidden;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/* The first band title is the LCP text. On a phone the hero padding and the
|
|
78
|
+
gap under the copy were pushing that title just past the first viewport,
|
|
79
|
+
so the paint waited on a later frame. */
|
|
80
|
+
@media (max-width: 767px) {
|
|
81
|
+
.home-band--hero {
|
|
82
|
+
padding-block: 1.75rem 1.25rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.home-hero {
|
|
86
|
+
gap: var(--space-md);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
.home-band--hero .home-band-inner {
|
|
78
91
|
position: relative;
|
|
79
92
|
z-index: 1;
|
|
@@ -25,6 +25,7 @@ module Jekyll
|
|
|
25
25
|
site.data["content_fragments"] ||= {}
|
|
26
26
|
|
|
27
27
|
load_page_content(site, content_path)
|
|
28
|
+
load_page_headers(site, content_path)
|
|
28
29
|
load_structured_data(site, content_path)
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -40,6 +41,17 @@ module Jekyll
|
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
# content/pages/headers.yml is also exported as pages/headers.json.
|
|
45
|
+
# The home bands read it here so a section title and note are in the
|
|
46
|
+
# HTML. A heading painted later by the runtime becomes the LCP element.
|
|
47
|
+
def load_page_headers(site, content_path)
|
|
48
|
+
path = File.join(content_path, "pages", "headers.yml")
|
|
49
|
+
return unless File.file?(path)
|
|
50
|
+
|
|
51
|
+
loaded = load_yaml(path)
|
|
52
|
+
site.data["page_headers"] = loaded.is_a?(Hash) ? loaded : {}
|
|
53
|
+
end
|
|
54
|
+
|
|
43
55
|
def load_structured_data(site, content_path)
|
|
44
56
|
data_path = File.join(content_path, "collections", "data")
|
|
45
57
|
return unless Dir.exist?(data_path)
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# One Lighthouse mobile run against the built example home.
|
|
3
|
+
# Thresholds apply to releases. LIGHTHOUSE_MODE=enforce fails below 75
|
|
4
|
+
# and warns below 90. LIGHTHOUSE_MODE=warn never fails the run. PLT-AC-24.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
+
SITE="${ROOT}/examples/minimal/_site"
|
|
9
|
+
PORT="${LIGHTHOUSE_PORT:-4173}"
|
|
10
|
+
OUT="${LIGHTHOUSE_REPORT:-/tmp/lighthouse-home.json}"
|
|
11
|
+
FAIL_BELOW=75
|
|
12
|
+
WARN_BELOW=90
|
|
13
|
+
MODE="${LIGHTHOUSE_MODE:-enforce}"
|
|
14
|
+
|
|
15
|
+
fail() {
|
|
16
|
+
echo "ERROR: $1" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
step() { echo "==> $1"; }
|
|
21
|
+
|
|
22
|
+
if [[ "${MODE}" != "enforce" && "${MODE}" != "warn" ]]; then
|
|
23
|
+
fail "LIGHTHOUSE_MODE must be enforce or warn (got ${MODE})"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
[[ -f "${SITE}/index.html" ]] || fail "missing ${SITE}/index.html (run scripts/validate.sh first)"
|
|
27
|
+
|
|
28
|
+
if [[ -n "${CHROME_PATH:-}" && -x "${CHROME_PATH}" ]]; then
|
|
29
|
+
CHROME="${CHROME_PATH}"
|
|
30
|
+
elif command -v google-chrome >/dev/null 2>&1; then
|
|
31
|
+
CHROME="$(command -v google-chrome)"
|
|
32
|
+
elif command -v google-chrome-stable >/dev/null 2>&1; then
|
|
33
|
+
CHROME="$(command -v google-chrome-stable)"
|
|
34
|
+
else
|
|
35
|
+
fail "Chrome is not installed"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
step "Serving ${SITE} (gzip, concurrent)"
|
|
39
|
+
python3 "${ROOT}/scripts/lighthouse_server.py" "${PORT}" "${SITE}" >/tmp/lighthouse-home-server.log 2>&1 &
|
|
40
|
+
SERVER_PID=$!
|
|
41
|
+
cleanup() {
|
|
42
|
+
kill "${SERVER_PID}" >/dev/null 2>&1 || true
|
|
43
|
+
}
|
|
44
|
+
trap cleanup EXIT
|
|
45
|
+
|
|
46
|
+
ready=0
|
|
47
|
+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
|
|
48
|
+
if curl -sf "http://127.0.0.1:${PORT}/" >/dev/null; then
|
|
49
|
+
ready=1
|
|
50
|
+
break
|
|
51
|
+
fi
|
|
52
|
+
sleep 0.25
|
|
53
|
+
done
|
|
54
|
+
[[ "${ready}" == "1" ]] || fail "example server did not answer on port ${PORT}"
|
|
55
|
+
|
|
56
|
+
step "Lighthouse mobile (one run)"
|
|
57
|
+
rm -f "${OUT}"
|
|
58
|
+
npx --yes lighthouse@13.5.0 "http://127.0.0.1:${PORT}/" \
|
|
59
|
+
--only-categories=performance \
|
|
60
|
+
--form-factor=mobile \
|
|
61
|
+
--output=json \
|
|
62
|
+
--output-path="${OUT}" \
|
|
63
|
+
--quiet \
|
|
64
|
+
--no-enable-error-reporting \
|
|
65
|
+
--chrome-path="${CHROME}" \
|
|
66
|
+
--chrome-flags="--headless=new --no-sandbox --disable-gpu"
|
|
67
|
+
|
|
68
|
+
python3 - "${OUT}" "${FAIL_BELOW}" "${WARN_BELOW}" "${MODE}" <<'PY'
|
|
69
|
+
import json
|
|
70
|
+
import sys
|
|
71
|
+
|
|
72
|
+
path, fail_below, warn_below, mode = sys.argv[1:]
|
|
73
|
+
fail_below = int(fail_below)
|
|
74
|
+
warn_below = int(warn_below)
|
|
75
|
+
report = json.load(open(path, encoding="utf-8"))
|
|
76
|
+
raw = report.get("categories", {}).get("performance", {}).get("score")
|
|
77
|
+
if raw is None:
|
|
78
|
+
print("ERROR: Lighthouse returned no performance score", file=sys.stderr)
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
score = int(round(float(raw) * 100))
|
|
81
|
+
print(f"Lighthouse mobile performance: {score} ({mode})")
|
|
82
|
+
campaign = (
|
|
83
|
+
"A score from 75 to 89 is acceptable only to ship a bug fix. "
|
|
84
|
+
"An urgent performance campaign must bring it back to 90 or above."
|
|
85
|
+
)
|
|
86
|
+
if score < fail_below and mode == "enforce":
|
|
87
|
+
print(f"::error::Lighthouse mobile performance {score} is below {fail_below}")
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
if score < fail_below:
|
|
90
|
+
print(
|
|
91
|
+
f"::warning::Lighthouse mobile performance {score} is below {fail_below}. "
|
|
92
|
+
"Thresholds apply to releases. This run only warns."
|
|
93
|
+
)
|
|
94
|
+
if score < warn_below:
|
|
95
|
+
print(
|
|
96
|
+
f"::warning::Lighthouse mobile performance {score} is below {warn_below}. "
|
|
97
|
+
+ campaign
|
|
98
|
+
)
|
|
99
|
+
PY
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Serve a built site the way the public host does.
|
|
3
|
+
|
|
4
|
+
Python's stock http.server speaks HTTP/1.0 on one thread and sends HTML
|
|
5
|
+
and CSS uncompressed. Lighthouse then scores the same templates far below
|
|
6
|
+
the live site, which compresses responses and serves them concurrently.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import gzip
|
|
10
|
+
import io
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
14
|
+
|
|
15
|
+
COMPRESSIBLE_EXACT = {
|
|
16
|
+
"application/javascript",
|
|
17
|
+
"application/json",
|
|
18
|
+
"application/xml",
|
|
19
|
+
"image/svg+xml",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compressible(ctype):
|
|
24
|
+
return ctype.startswith("text/") or ctype in COMPRESSIBLE_EXACT
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Handler(SimpleHTTPRequestHandler):
|
|
28
|
+
protocol_version = "HTTP/1.1"
|
|
29
|
+
|
|
30
|
+
def send_head(self):
|
|
31
|
+
path = self.translate_path(self.path)
|
|
32
|
+
if os.path.isdir(path):
|
|
33
|
+
index = os.path.join(path, "index.html")
|
|
34
|
+
if os.path.isfile(index):
|
|
35
|
+
path = index
|
|
36
|
+
else:
|
|
37
|
+
return super().send_head()
|
|
38
|
+
|
|
39
|
+
ctype = self.guess_type(path)
|
|
40
|
+
accept = self.headers.get("Accept-Encoding", "")
|
|
41
|
+
if "gzip" not in accept or not compressible(ctype):
|
|
42
|
+
return super().send_head()
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
with open(path, "rb") as raw:
|
|
46
|
+
data = raw.read()
|
|
47
|
+
except OSError:
|
|
48
|
+
self.send_error(404, "File not found")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
body = gzip.compress(data, compresslevel=6)
|
|
52
|
+
self.send_response(200)
|
|
53
|
+
self.send_header("Content-Type", ctype)
|
|
54
|
+
self.send_header("Content-Encoding", "gzip")
|
|
55
|
+
self.send_header("Vary", "Accept-Encoding")
|
|
56
|
+
self.send_header("Content-Length", str(len(body)))
|
|
57
|
+
self.end_headers()
|
|
58
|
+
return io.BytesIO(body)
|
|
59
|
+
|
|
60
|
+
def log_message(self, fmt, *args):
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
if len(sys.argv) != 3:
|
|
66
|
+
print("usage: lighthouse_server.py PORT DIRECTORY", file=sys.stderr)
|
|
67
|
+
sys.exit(2)
|
|
68
|
+
port = int(sys.argv[1])
|
|
69
|
+
os.chdir(sys.argv[2])
|
|
70
|
+
ThreadingHTTPServer(("127.0.0.1", port), Handler).serve_forever()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Lighthouse mobile release rule and the non-blocking icon sheet.
|
|
5
|
+
# PLT-AC-24, PLT-AC-25.
|
|
6
|
+
|
|
7
|
+
require "pathname"
|
|
8
|
+
|
|
9
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
10
|
+
|
|
11
|
+
def fail!(message)
|
|
12
|
+
warn "FAIL test-lighthouse-policy: #{message}"
|
|
13
|
+
exit 1
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
docs = {
|
|
17
|
+
"spec" => ROOT.join("docs/spec.md").read,
|
|
18
|
+
"contributing" => ROOT.join("CONTRIBUTING.md").read,
|
|
19
|
+
"readme" => ROOT.join("README.md").read
|
|
20
|
+
}
|
|
21
|
+
shell = ROOT.join("_layouts/shell.html").read
|
|
22
|
+
base = ROOT.join("assets/css/base.css").read
|
|
23
|
+
script = ROOT.join("scripts/lighthouse-home.sh").read
|
|
24
|
+
workflow = ROOT.join(".github/workflows/validate.yml").read
|
|
25
|
+
release = ROOT.join(".github/workflows/publish-gem.yml").read
|
|
26
|
+
|
|
27
|
+
docs.each do |name, text|
|
|
28
|
+
flat = text.gsub(/\s+/, " ")
|
|
29
|
+
fail!("#{name} does not require 90 or above") unless flat.include?("90 or above")
|
|
30
|
+
fail!("#{name} does not limit 75 to 89 to a bug fix") unless flat.include?("only to ship a bug fix")
|
|
31
|
+
fail!("#{name} does not require the performance campaign") unless flat.include?("urgent performance campaign")
|
|
32
|
+
fail!("#{name} does not forbid a score below 75") unless flat.include?("Below 75 is never acceptable")
|
|
33
|
+
fail!("#{name} does not limit the floors to releases") unless flat.include?("apply to releases")
|
|
34
|
+
fail!("#{name} does not allow a lower pull-request score") unless flat.include?("may score lower")
|
|
35
|
+
fail!("#{name} does not allow modular work") unless flat.include?("modular")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
spec = docs.fetch("spec")
|
|
39
|
+
contributing = docs.fetch("contributing")
|
|
40
|
+
|
|
41
|
+
fail!("spec dropped the slower-release reason") unless spec.include?("slower release is better than a broken one")
|
|
42
|
+
fail!("contributing dropped the slower-release reason") unless contributing.include?("slower release is better than a broken one")
|
|
43
|
+
|
|
44
|
+
fail!("lighthouse script is not the CI check") unless workflow.include?("./scripts/lighthouse-home.sh")
|
|
45
|
+
fail!("pull requests still fail the lighthouse gate") unless workflow.include?("LIGHTHOUSE_MODE: warn")
|
|
46
|
+
fail!("release build does not enforce the score") unless release.include?("LIGHTHOUSE_MODE: enforce")
|
|
47
|
+
fail!("tag builds do not run the release gate") unless release.include?('tags: ["v*"]')
|
|
48
|
+
fail!("lighthouse script lost the fail floor") unless script.include?("FAIL_BELOW=75")
|
|
49
|
+
fail!("lighthouse script lost the warn floor") unless script.include?("WARN_BELOW=90")
|
|
50
|
+
fail!("lighthouse script lost enforce mode") unless script.include?('LIGHTHOUSE_MODE:-enforce')
|
|
51
|
+
fail!("warn mode still fails the run") unless script.include?('mode == "enforce"')
|
|
52
|
+
fail!("lighthouse script is not a single mobile run") unless script.include?("--form-factor=mobile")
|
|
53
|
+
fail!("lighthouse script repeats runs") if script.include?("--repeat")
|
|
54
|
+
fail!("example server is still one-thread HTTP/1.0") if script.include?("python3 -m http.server")
|
|
55
|
+
server = ROOT.join("scripts/lighthouse_server.py").read
|
|
56
|
+
fail!("example server does not gzip") unless server.include?('Content-Encoding", "gzip"')
|
|
57
|
+
fail!("example server is not concurrent") unless server.include?("ThreadingHTTPServer")
|
|
58
|
+
fail!("icon face is preloaded") if shell.include?("rel=\"preload\" as=\"style\" href=\"{{ _symbols_href }}\"")
|
|
59
|
+
|
|
60
|
+
screen = shell.sub(%r{<noscript>.*?</noscript>}m, "")
|
|
61
|
+
fail!("Material Symbols sheet is missing") unless shell.include?("Material+Symbols+Outlined")
|
|
62
|
+
fail!("icon sheet is not deferred until load") unless screen.include?("addEventListener('load'")
|
|
63
|
+
fail!("screen still has a Material Symbols stylesheet link") if screen.match?(/<link\b[^>]*Material\+Symbols/)
|
|
64
|
+
|
|
65
|
+
icon = base[/\.material-symbols-outlined\.material-symbols-outlined \{.*?\n\}/m]
|
|
66
|
+
fail!("icon rule missing") unless icon
|
|
67
|
+
fail!("icon box is not 1em") unless icon.include?("width: 1em;") && icon.include?("height: 1em;")
|
|
68
|
+
fail!("icon size can still jump when the Google sheet arrives") unless icon.include?("font-size: inherit;")
|
|
69
|
+
fail!("icon box does not clip the fallback word") unless icon.include?("overflow: hidden;")
|
|
70
|
+
fail!("content runtime still precedes the page") unless shell.index("{{ content }}") && shell.index("page/content-runtime.html") > shell.index("{{ content }}")
|
|
71
|
+
band = File.read(File.expand_path("../../_includes/home/band-listing.html", __dir__))
|
|
72
|
+
fail!("band title is still painted by script") unless band.include?('data-content-section-title>{{ _title }}')
|
|
73
|
+
fail!("band note is still painted by script") unless band.include?("page_headers")
|
|
74
|
+
headers = File.read(File.expand_path("../../_plugins/content_headers.rb", __dir__))
|
|
75
|
+
fail!("page headers are not loaded at build time") unless headers.include?('site.data["page_headers"]')
|
|
76
|
+
home = File.read(File.expand_path("../../_layouts/home.html", __dir__))
|
|
77
|
+
fail!("hero subtitle is still painted by script") unless home.include?("page_headers.home") && home.include?("data-content-static")
|
|
78
|
+
|
|
79
|
+
puts "PASS test-lighthouse-policy"
|
data/scripts/validate.sh
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pandorga
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.6.
|
|
4
|
+
version: 1.6.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Pandorga contributors
|
|
@@ -322,6 +322,8 @@ files:
|
|
|
322
322
|
- scripts/lib/r2_json_sync.rb
|
|
323
323
|
- scripts/lib/withdrawn_public_objects.rb
|
|
324
324
|
- scripts/lib/writing_index_contract.rb
|
|
325
|
+
- scripts/lighthouse-home.sh
|
|
326
|
+
- scripts/lighthouse_server.py
|
|
325
327
|
- scripts/pdf/md-to-pdf.mjs
|
|
326
328
|
- scripts/pdf/md-to-pdf.sh
|
|
327
329
|
- scripts/tailwind/build.sh
|
|
@@ -412,6 +414,7 @@ files:
|
|
|
412
414
|
- scripts/test/test-install-functions.rb
|
|
413
415
|
- scripts/test/test-language-export.rb
|
|
414
416
|
- scripts/test/test-ledger-blocks.rb
|
|
417
|
+
- scripts/test/test-lighthouse-policy.rb
|
|
415
418
|
- scripts/test/test-listing-filter-contract.rb
|
|
416
419
|
- scripts/test/test-math-row-breaks.mjs
|
|
417
420
|
- scripts/test/test-math-row-breaks.rb
|