jekyll-theme-zer0 1.28.0 → 1.30.0
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 +1143 -13
- data/_data/README.md +2 -0
- data/_data/ai.yml +5 -3
- data/_data/ai_pricing.yml +36 -0
- data/_data/backlog.yml +507 -2
- data/_data/consumers.yml +157 -9
- data/_data/features.yml +303 -20
- data/_data/feedback_types.yml +17 -12
- data/_data/i18n/fr.yml +12 -7
- data/_data/i18n/manifest.yml +39 -12
- data/_data/ingredient_densities.yml +122 -0
- data/_data/landing.yml +5 -2
- data/_data/navigation/main.yml +16 -0
- data/_data/navigation/quickstart.yml +4 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/site_builder.yml +874 -0
- data/_data/theme-manifest.yml +160 -124
- data/_data/ui-text.yml +26 -0
- data/_includes/README.md +26 -2
- data/_includes/analytics/posthog.html +2 -2
- data/_includes/components/admin-links.html +2 -2
- data/_includes/components/admin-tabs.html +2 -2
- data/_includes/components/ai-chat.html +14 -11
- data/_includes/components/analytics-dashboard.html +8 -8
- data/_includes/components/author-bio.html +1 -1
- data/_includes/components/author-card.html +10 -2
- data/_includes/components/author-eeat.html +4 -4
- data/_includes/components/background-customizer.html +10 -10
- data/_includes/components/background-image.html +114 -0
- data/_includes/components/background-settings.html +28 -15
- data/_includes/components/collection-manager.html +5 -5
- data/_includes/components/component-showcase.html +13 -13
- data/_includes/components/config-editor.html +12 -12
- data/_includes/components/config-viewer.html +8 -8
- data/_includes/components/cookie-consent.html +15 -15
- data/_includes/components/cta-button.html +7 -2
- data/_includes/components/dev-shortcuts.html +7 -7
- data/_includes/components/env-dashboard.html +8 -8
- data/_includes/components/env-switcher.html +9 -9
- data/_includes/components/feature-card.html +2 -2
- data/_includes/components/halfmoon.html +2 -2
- data/_includes/components/info-section.html +42 -37
- data/_includes/components/js-cdn.html +15 -15
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/mermaid.html +72 -435
- data/_includes/components/nanobar.html +5 -5
- data/_includes/components/nav-editor.html +2 -2
- data/_includes/components/nav-export.html +2 -2
- data/_includes/components/nav-overview.html +2 -2
- data/_includes/components/page-feedback.html +45 -30
- data/_includes/components/page-views-init.html +55 -0
- data/_includes/components/page-views.html +33 -0
- data/_includes/components/post-card.html +22 -22
- data/_includes/components/post-type-badge.html +2 -2
- data/_includes/components/powered-by.html +2 -2
- data/_includes/components/preview-image.html +6 -0
- data/_includes/components/quick-index.html +2 -2
- data/_includes/components/recipe-card.html +67 -0
- data/_includes/components/recipe-duration.html +50 -0
- data/_includes/components/recipe-grams.html +58 -0
- data/_includes/components/recipe-index.html +96 -0
- data/_includes/components/recipe-ingredients.html +90 -0
- data/_includes/components/recipe-meta.html +96 -0
- data/_includes/components/recipe-nutrition.html +57 -0
- data/_includes/components/recipe-qty.html +73 -0
- data/_includes/components/recipe-ratio.html +151 -0
- data/_includes/components/recipe-scaler.html +73 -0
- data/_includes/components/recipe-steps.html +86 -0
- data/_includes/components/recipe-temp.html +45 -0
- data/_includes/components/search-modal.html +29 -4
- data/_includes/components/searchbar.html +2 -2
- data/_includes/components/shortcuts-modal.html +3 -0
- data/_includes/components/svg-background.html +2 -2
- data/_includes/components/theme-customizer.html +2 -2
- data/_includes/components/theme-info.html +14 -7
- data/_includes/components/theme-preview-gallery.html +22 -22
- data/_includes/content/giscus.html +2 -2
- data/_includes/content/intro.html +8 -8
- data/_includes/content/jsonld-faq.html +2 -2
- data/_includes/content/jsonld-software.html +24 -5
- data/_includes/content/seo.html +4 -4
- data/_includes/content/sitemap.html +27 -27
- data/_includes/content/toc.html +183 -183
- data/_includes/core/branding.html +6 -6
- data/_includes/core/console-capture.html +32 -74
- data/_includes/core/favicon.html +49 -7
- data/_includes/core/footer-fabs.html +17 -3
- data/_includes/core/footer.html +49 -34
- data/_includes/core/head.html +110 -86
- data/_includes/core/header.html +76 -54
- data/_includes/docs/bootstrap-docs.html +8 -8
- data/_includes/landing/landing-install-cards.html +2 -2
- data/_includes/landing/landing-quick-links.html +1 -1
- data/_includes/navigation/admin-nav.html +2 -2
- data/_includes/navigation/nav-tree.html +8 -8
- data/_includes/navigation/navbar.html +12 -12
- data/_includes/navigation/section-sidebar.html +109 -27
- data/_includes/navigation/sidebar-config.html +36 -2
- data/_includes/navigation/sidebar-left.html +17 -16
- data/_includes/navigation/sidebar-right.html +8 -7
- data/_includes/obsidian/full-graph.html +2 -2
- data/_includes/setup/claude-session.html +72 -0
- data/_includes/setup/prereq-checklist.html +90 -0
- data/_includes/setup/wizard.html +924 -222
- data/_includes/stats/stats-categories.html +8 -8
- data/_includes/stats/stats-header.html +14 -14
- data/_includes/stats/stats-metrics.html +14 -14
- data/_includes/stats/stats-no-data.html +12 -12
- data/_includes/stats/stats-overview.html +6 -6
- data/_includes/stats/stats-tags.html +8 -8
- data/_layouts/404.html +38 -24
- data/_layouts/README.md +2 -0
- data/_layouts/admin.html +24 -24
- data/_layouts/article.html +43 -33
- data/_layouts/author.html +20 -20
- data/_layouts/authors.html +2 -2
- data/_layouts/book-abc.html +12 -12
- data/_layouts/book-story.html +15 -15
- data/_layouts/book.html +12 -12
- data/_layouts/collection.html +33 -33
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +31 -28
- data/_layouts/home.html +23 -23
- data/_layouts/index.html +10 -10
- data/_layouts/landing.html +17 -17
- data/_layouts/news.html +44 -44
- data/_layouts/note.html +38 -38
- data/_layouts/notebook.html +34 -34
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +92 -55
- data/_layouts/section.html +62 -33
- data/_layouts/setup.html +3 -3
- data/_layouts/sitemap-collection.html +49 -49
- data/_layouts/stats.html +40 -40
- data/_layouts/tag.html +12 -12
- data/_layouts/welcome.html +21 -21
- data/_sass/components/_callout.scss +1 -1
- data/_sass/components/_footer.scss +37 -1
- data/_sass/components/_mermaid.scss +375 -0
- data/_sass/components/_page-views.scss +36 -0
- data/_sass/components/_recipe.scss +506 -0
- data/_sass/components/_setup-wizard.scss +764 -0
- data/_sass/components/_ui-enhancements.scss +6 -6
- data/_sass/core/_navbar.scss +261 -46
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/layouts/_navbar-extras.scss +14 -4
- data/_sass/tokens/_color.scss +6 -0
- data/_sass/tokens/_index.scss +2 -0
- data/_sass/tokens/_radius.scss +21 -0
- data/_sass/tokens/_typography.scss +4 -0
- data/_sass/utilities/_focus.scss +14 -0
- data/assets/css/main.scss +4 -0
- data/assets/js/ai-chat.js +47 -5
- data/assets/js/fleet-feedback-capture.js +124 -0
- data/assets/js/fleet-feedback.js +853 -0
- data/assets/js/mermaid-diagrams.js +1267 -0
- data/assets/js/modules/navigation/config.js +9 -6
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/modules/navigation/scroll-spy.js +315 -80
- data/assets/js/modules/theme/appearance.js +8 -2
- data/assets/js/obsidian-wiki-links.js +8 -3
- data/assets/js/page-feedback.js +125 -192
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +36 -0
- data/assets/js/setup-wizard.js +2279 -226
- data/assets/js/site-builder.js +1834 -0
- data/assets/js/ui-enhancements.js +11 -3
- data/scripts/README.md +44 -0
- data/scripts/ai/README.md +38 -0
- data/scripts/ai/api_call.rb +124 -0
- data/scripts/ai/usage.rb +314 -0
- data/scripts/ai/usage_report.rb +225 -0
- data/scripts/bin/audit-consumer +39 -7
- data/scripts/bin/giscus-discussions +213 -14
- data/scripts/bin/manifest +35 -12
- data/scripts/ci/agent_review_result.py +164 -0
- data/scripts/ci/test_agent_review_result.py +172 -0
- data/scripts/ci/test_visual_evidence_autogen.py +341 -0
- data/scripts/ci/visual_evidence_autogen.py +1060 -0
- data/scripts/content-review.rb +20 -1
- data/scripts/design-system-check.rb +170 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/test/integration/mermaid +22 -8
- data/scripts/test/lib/run_tests.sh +3 -1
- data/scripts/test/lib/test_agent_review_result.sh +27 -0
- data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
- data/scripts/translate.rb +94 -16
- metadata +48 -2
data/scripts/content-review.rb
CHANGED
|
@@ -348,7 +348,26 @@ def check_quality(body, quality)
|
|
|
348
348
|
|
|
349
349
|
# Bare URLs.
|
|
350
350
|
if quality['flag_bare_urls']
|
|
351
|
-
|
|
351
|
+
# Two sources of false positives this guards against:
|
|
352
|
+
# [http://x](http://x) — a CORRECT markdown link whose text is the URL.
|
|
353
|
+
# The lookbehind must exclude `[` too, or the link text is counted.
|
|
354
|
+
# <script>/<style> blocks and HTML attributes — pages that embed a config
|
|
355
|
+
# generator carry URLs in JS strings and markup, which are code, not prose.
|
|
356
|
+
# <code>http://…</code> — inline code in HTML syntax, which the markdown
|
|
357
|
+
# backtick form is already exempt from; the two should agree.
|
|
358
|
+
# Stripping markup in ONE pass leaves split or nested tags behind (CodeQL:
|
|
359
|
+
# incomplete multi-character sanitization). This is prose extraction for
|
|
360
|
+
# counting, never sanitization for output, but iterate to a fixpoint anyway
|
|
361
|
+
# so the result is exact. Each pass strictly shortens the string, so it
|
|
362
|
+
# terminates.
|
|
363
|
+
prose = strip_code_fences(body)
|
|
364
|
+
loop do
|
|
365
|
+
before = prose
|
|
366
|
+
prose = prose.gsub(%r{<(script|style|code|pre)\b[^>]*>.*?</\1\s*>}mi, ' ')
|
|
367
|
+
.gsub(/<[^<>]*>/, ' ')
|
|
368
|
+
break if prose == before
|
|
369
|
+
end
|
|
370
|
+
bare = prose.scan(%r{(?<![("<\[\]])\bhttps?://[^\s)>\]]+}).reject { |u| u.include?('](') }
|
|
352
371
|
issues << Issue.new('info', 'quality', "#{bare.length} bare URL(s) — wrap as [text](url)") unless bare.empty?
|
|
353
372
|
end
|
|
354
373
|
issues
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# Feature: ZER0-081
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# ---------------------------------------------------------------------------
|
|
6
|
+
# design-system-check — token parity between the theme and its Claude Design
|
|
7
|
+
# mirror.
|
|
8
|
+
#
|
|
9
|
+
# The `--zer0-*` design tokens live twice on purpose:
|
|
10
|
+
# - `_sass/tokens/*.scss` — the source of truth the theme compiles
|
|
11
|
+
# - `_design-system/tokens/*.css` — the mirror pushed to the Claude Design
|
|
12
|
+
# project (see _design-system/SYNC.md)
|
|
13
|
+
#
|
|
14
|
+
# This check keeps the two in lockstep so the design system can be built from
|
|
15
|
+
# either side without silent drift:
|
|
16
|
+
#
|
|
17
|
+
# 1. Per file pair: the sets of `--zer0-*` names must match, and every value
|
|
18
|
+
# must resolve to the same string. `var(--x, fallback)` wrappers are
|
|
19
|
+
# resolved against the mirror's concrete `--bs-*`/`--bd-*` primitives, so
|
|
20
|
+
# the theme's alias form and the mirror's self-contained form compare
|
|
21
|
+
# equal.
|
|
22
|
+
# 2. Skins: `_sass/theme/_skins.scss` and `tokens/skins.css` must declare the
|
|
23
|
+
# same skin names, and each mirror skin's brand hex must appear in the
|
|
24
|
+
# matching SCSS block.
|
|
25
|
+
# 3. Dangling-token audit: any fallback-less `var(--zer0-…)` reference in
|
|
26
|
+
# `_sass/` must be declared somewhere (tokens, includes, or JS).
|
|
27
|
+
#
|
|
28
|
+
# ruby scripts/design-system-check.rb # exit 1 + report on drift
|
|
29
|
+
#
|
|
30
|
+
# Runs in the `core` test suite (test/test_core.sh). Plain Ruby, no gems.
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
ROOT = File.expand_path('..', __dir__)
|
|
34
|
+
|
|
35
|
+
PAIRS = {
|
|
36
|
+
'_sass/tokens/_color.scss' => '_design-system/tokens/colors.css',
|
|
37
|
+
'_sass/tokens/_spacing.scss' => '_design-system/tokens/spacing.css',
|
|
38
|
+
'_sass/tokens/_radius.scss' => '_design-system/tokens/radius.css',
|
|
39
|
+
'_sass/tokens/_typography.scss' => '_design-system/tokens/typography.css',
|
|
40
|
+
'_sass/tokens/_shadow.scss' => '_design-system/tokens/shadows.css',
|
|
41
|
+
'_sass/tokens/_motion.scss' => '_design-system/tokens/motion.css',
|
|
42
|
+
'_sass/tokens/_breakpoints.scss' => '_design-system/tokens/breakpoints.css',
|
|
43
|
+
'_sass/tokens/_layers.scss' => '_design-system/tokens/layers.css'
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
SKIN_SCSS = '_sass/theme/_skins.scss'
|
|
47
|
+
SKIN_CSS = '_design-system/tokens/skins.css'
|
|
48
|
+
|
|
49
|
+
def read(rel)
|
|
50
|
+
path = File.join(ROOT, rel)
|
|
51
|
+
abort "design-system-check: missing file #{rel}" unless File.file?(path)
|
|
52
|
+
File.read(path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Strip /* */ blocks, // line comments, and everything from the first @media on
|
|
56
|
+
# (motion files re-declare durations under prefers-reduced-motion).
|
|
57
|
+
def strip(text, scss:)
|
|
58
|
+
t = text.split('@media', 2).first
|
|
59
|
+
t = t.gsub(%r{/\*.*?\*/}m, '')
|
|
60
|
+
t = t.gsub(%r{(?:^|\s)//.*$}, '') if scss
|
|
61
|
+
t
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# { '--zer0-foo' => 'raw value', ... } from declaration-position matches.
|
|
65
|
+
def declarations(text)
|
|
66
|
+
text.scan(/(--zer0-[\w-]+)\s*:\s*([^;]+);/).each_with_object({}) do |(k, v), h|
|
|
67
|
+
h[k] ||= v.strip.gsub(/\s+/, ' ')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# All custom properties (any prefix) declared in the mirror's colors.css light
|
|
72
|
+
# scope — the concrete primitives var() chains resolve against.
|
|
73
|
+
def primitives
|
|
74
|
+
light = read(SKIN_CSS.sub('skins', 'colors')).split('[data-bs-theme="dark"]', 2).first
|
|
75
|
+
strip(light, scss: false).scan(/(--[\w-]+)\s*:\s*([^;]+);/)
|
|
76
|
+
.each_with_object({}) { |(k, v), h| h[k] ||= v.strip.gsub(/\s+/, ' ') }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Resolve a value that is entirely a var() wrapper: prefer the primitive's
|
|
80
|
+
# concrete value, else the wrapper's own fallback. Inner var() references in
|
|
81
|
+
# composite values (color-mix, rgba) are left as-is and compared literally.
|
|
82
|
+
def resolve(value, prims, depth = 0)
|
|
83
|
+
v = value.strip.gsub(/\s+/, ' ')
|
|
84
|
+
return v if depth > 6
|
|
85
|
+
|
|
86
|
+
m = v.match(/\Avar\((--[\w-]+)(?:\s*,\s*(.+))?\)\z/)
|
|
87
|
+
return v unless m
|
|
88
|
+
|
|
89
|
+
name, fallback = m[1], m[2]
|
|
90
|
+
if prims.key?(name)
|
|
91
|
+
resolve(prims[name], prims, depth + 1)
|
|
92
|
+
elsif fallback
|
|
93
|
+
resolve(fallback, prims, depth + 1)
|
|
94
|
+
else
|
|
95
|
+
v
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
failures = []
|
|
100
|
+
prims = primitives
|
|
101
|
+
|
|
102
|
+
# --- 1. token parity per file pair -----------------------------------------
|
|
103
|
+
PAIRS.each do |scss_rel, css_rel|
|
|
104
|
+
scss = declarations(strip(read(scss_rel), scss: true))
|
|
105
|
+
css = declarations(strip(read(css_rel), scss: false))
|
|
106
|
+
|
|
107
|
+
(scss.keys - css.keys).each { |k| failures << "#{css_rel}: missing #{k} (declared in #{scss_rel})" }
|
|
108
|
+
(css.keys - scss.keys).each { |k| failures << "#{scss_rel}: missing #{k} (declared in #{css_rel})" }
|
|
109
|
+
|
|
110
|
+
(scss.keys & css.keys).each do |k|
|
|
111
|
+
a = resolve(scss[k], prims)
|
|
112
|
+
b = resolve(css[k], prims)
|
|
113
|
+
failures << "#{k}: theme resolves to `#{a}` but mirror to `#{b}`" unless a == b
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Both motion files must keep the reduced-motion guard.
|
|
118
|
+
[PAIRS.key('_design-system/tokens/motion.css'), '_design-system/tokens/motion.css'].each do |rel|
|
|
119
|
+
failures << "#{rel}: lost its prefers-reduced-motion guard" unless read(rel).include?('prefers-reduced-motion')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# --- 2. skins: same names, same brand hex ----------------------------------
|
|
123
|
+
scss_skins_src = read(SKIN_SCSS)
|
|
124
|
+
css_skins_src = read(SKIN_CSS)
|
|
125
|
+
scss_skins = scss_skins_src.scan(/\[data-theme-skin="(\w+)"\]/).flatten.uniq.sort
|
|
126
|
+
css_skins = css_skins_src.scan(/\[data-theme-skin="(\w+)"\]/).flatten.uniq.sort
|
|
127
|
+
unless scss_skins == css_skins
|
|
128
|
+
failures << "skins differ: theme has #{scss_skins.join(', ')} but mirror has #{css_skins.join(', ')}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
(scss_skins & css_skins).each do |skin|
|
|
132
|
+
block = css_skins_src[/\[data-theme-skin="#{skin}"\]\s*\{.*?\}/m].to_s
|
|
133
|
+
hex = block[/--zer0-color-primary:\s*(#\h+)/, 1]
|
|
134
|
+
next unless hex
|
|
135
|
+
|
|
136
|
+
scss_block = scss_skins_src[/\[data-theme-skin="#{skin}"\]\s*\{.*?\}/m].to_s
|
|
137
|
+
unless scss_block.downcase.include?(hex.downcase)
|
|
138
|
+
failures << "skin #{skin}: mirror brand #{hex} not found in #{SKIN_SCSS}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# --- 3. dangling-token audit ------------------------------------------------
|
|
143
|
+
declared = Dir.glob(File.join(ROOT, '_sass/**/*.scss'))
|
|
144
|
+
.flat_map { |f| File.read(f).scan(/(--zer0-[\w-]+)\s*:/) }.flatten
|
|
145
|
+
declared += Dir.glob(File.join(ROOT, '{_includes,_layouts}/**/*.html'))
|
|
146
|
+
.flat_map { |f| File.read(f).scan(/(--zer0-[\w-]+)\s*:/) }.flatten
|
|
147
|
+
declared += Dir.glob(File.join(ROOT, 'assets/js/**/*.js')).flat_map do |f|
|
|
148
|
+
src = File.read(f)
|
|
149
|
+
src.scan(/setProperty\(\s*['"](--zer0-[\w-]+)/).flatten +
|
|
150
|
+
src.scan(/['"`](--zer0-[\w-]+)['"`:]/).flatten
|
|
151
|
+
end
|
|
152
|
+
declared = declared.uniq
|
|
153
|
+
|
|
154
|
+
Dir.glob(File.join(ROOT, '_sass/**/*.scss')).sort.each do |f|
|
|
155
|
+
File.read(f).scan(/var\(\s*(--zer0-[\w-]+)\s*\)/).flatten.uniq.each do |token|
|
|
156
|
+
next if declared.include?(token)
|
|
157
|
+
|
|
158
|
+
failures << "#{f.sub("#{ROOT}/", '')}: references #{token} with no fallback, but it is never declared"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# --- report -----------------------------------------------------------------
|
|
163
|
+
if failures.empty?
|
|
164
|
+
puts "design-system-check: OK — #{PAIRS.size} token file pairs, #{css_skins.size} skins, no dangling tokens"
|
|
165
|
+
else
|
|
166
|
+
warn "design-system-check: #{failures.size} problem(s) — theme (_sass/tokens) and mirror (_design-system/tokens) have drifted:"
|
|
167
|
+
failures.each { |f| warn " - #{f}" }
|
|
168
|
+
warn 'Fix the drift (see _design-system/SYNC.md), then re-run: ruby scripts/design-system-check.rb'
|
|
169
|
+
exit 1
|
|
170
|
+
end
|
data/scripts/lib/audit.sh
CHANGED
|
@@ -205,13 +205,45 @@ detect_consumer_mode() {
|
|
|
205
205
|
return
|
|
206
206
|
fi
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
# Match a top-level `remote_theme` key. The whitespace class before the
|
|
209
|
+
# colon is load-bearing: most fleet consumers write column-aligned YAML
|
|
210
|
+
# (`remote_theme : "bamr87/zer0-mistakes"`), which a bare
|
|
211
|
+
# `^remote_theme:` never matches — every one of them was silently
|
|
212
|
+
# misreported as gem mode, skipping the plugin checks entirely.
|
|
213
|
+
# `|| true` keeps a no-match grep from tripping the caller's `set -o pipefail`.
|
|
214
|
+
local line value
|
|
215
|
+
line=$(grep -m1 -E '^remote_theme[[:space:]]*:' "$config" 2>/dev/null || true)
|
|
216
|
+
value=$(printf '%s' "$line" \
|
|
217
|
+
| sed -E 's/^remote_theme[[:space:]]*:[[:space:]]*//; s/[[:space:]]*(#.*)?$//; s/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/')
|
|
218
|
+
|
|
219
|
+
# `remote_theme: false` (or an empty value) is how a consumer turns the
|
|
220
|
+
# remote theme OFF and resolves the theme as a gem instead.
|
|
221
|
+
if [[ -n "$value" && "$value" != "false" ]]; then
|
|
209
222
|
echo "remote_theme"
|
|
210
223
|
else
|
|
211
224
|
echo "gem"
|
|
212
225
|
fi
|
|
213
226
|
}
|
|
214
227
|
|
|
228
|
+
# detect_consumer_source <consumer_root>
|
|
229
|
+
# Echoes the Jekyll `source:` value from _config.yml (relative path), or empty.
|
|
230
|
+
#
|
|
231
|
+
# Jekyll's `source:` relocates the whole site tree — zer0-pages keeps every
|
|
232
|
+
# theme-shadowing file under `pages/`. Auditing the repo root there reports a
|
|
233
|
+
# falsely clean "0 overrides, 0 unique files"; the real surface only appears
|
|
234
|
+
# when the scan follows `source:`.
|
|
235
|
+
detect_consumer_source() {
|
|
236
|
+
local consumer_root="$1"
|
|
237
|
+
local config="$consumer_root/_config.yml"
|
|
238
|
+
|
|
239
|
+
[[ -f "$config" ]] || return 0
|
|
240
|
+
|
|
241
|
+
local line
|
|
242
|
+
line=$(grep -m1 -E '^source[[:space:]]*:' "$config" 2>/dev/null || true)
|
|
243
|
+
printf '%s' "$line" \
|
|
244
|
+
| sed -E 's/^source[[:space:]]*:[[:space:]]*//; s/[[:space:]]*(#.*)?$//; s/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/; s#^\./##; s#/$##'
|
|
245
|
+
}
|
|
246
|
+
|
|
215
247
|
# ---------------------------------------------------------------------------
|
|
216
248
|
# Output helpers
|
|
217
249
|
# ---------------------------------------------------------------------------
|
|
@@ -277,7 +309,15 @@ print_classification() {
|
|
|
277
309
|
echo "::notice file=$relpath::OPTIONAL_PLUGIN (not required)" ;;
|
|
278
310
|
esac
|
|
279
311
|
elif [[ "$format" == "json" ]]; then
|
|
280
|
-
|
|
312
|
+
# Separator goes BEFORE each entry but the first, so the array never
|
|
313
|
+
# ends on a dangling comma. Emitting a trailing comma per entry (the
|
|
314
|
+
# previous behaviour) produced `...},\n]}` — which no JSON parser
|
|
315
|
+
# accepts, making --format json unusable for any CI consumer.
|
|
316
|
+
if [[ "${JSON_ENTRY_EMITTED:-0}" == "1" ]]; then
|
|
317
|
+
printf ',\n'
|
|
318
|
+
fi
|
|
319
|
+
JSON_ENTRY_EMITTED=1
|
|
320
|
+
printf ' {"path":"%s","status":"%s"}' "$relpath" "$classification"
|
|
281
321
|
else
|
|
282
322
|
echo -e "${color}[$classification]${NC} $relpath"
|
|
283
323
|
fi
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ---------------------------------------------------------------------------
|
|
5
|
+
# lint-liquid-raw — guard the Markdown the Pages build renders through Liquid
|
|
6
|
+
#
|
|
7
|
+
# `github-pages` ships jekyll-optional-front-matter, so EVERY non-excluded
|
|
8
|
+
# Markdown file in the repo becomes a page — CHANGELOG.md and the
|
|
9
|
+
# _design-system/ mirror included — and every one of them is run through Liquid
|
|
10
|
+
# before Markdown. Two failure modes follow, and neither is loud:
|
|
11
|
+
#
|
|
12
|
+
# 1. An unbalanced `{% raw %}`. Liquid's raw block swallows everything up to
|
|
13
|
+
# the FIRST `{% endraw %}`, so a stray opener silently eats the rest of the
|
|
14
|
+
# document. CHANGELOG.md carried one that consumed 945 lines while the
|
|
15
|
+
# build stayed green.
|
|
16
|
+
#
|
|
17
|
+
# 2. A literal `{% raw %}` / `{% endraw %}` written as documentation prose.
|
|
18
|
+
# It reads like text but parses as a tag, which is how (1) happens. To show
|
|
19
|
+
# the tag, escape it: `{% raw %}{% raw %}{% endraw %}`.
|
|
20
|
+
#
|
|
21
|
+
# A Liquid *syntax* error (e.g. `{{…}}`) is caught by the Pages workflow, which
|
|
22
|
+
# fails on "Liquid Warning" in the build output. This script catches the class
|
|
23
|
+
# the build cannot report, because swallowed text is not an error to Liquid.
|
|
24
|
+
#
|
|
25
|
+
# Usage:
|
|
26
|
+
# scripts/lint-liquid-raw.rb # scan the rendered Markdown surface
|
|
27
|
+
# scripts/lint-liquid-raw.rb PATH... # scan specific files
|
|
28
|
+
# scripts/lint-liquid-raw.rb --verbose # list every file scanned
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
ROOT = File.expand_path('..', __dir__)
|
|
32
|
+
TAG = /\{%-?\s*(raw|endraw)\s*-?%\}/.freeze
|
|
33
|
+
|
|
34
|
+
# The escape form: a raw block whose entire body is one raw/endraw tag, which
|
|
35
|
+
# is how you render the tag as text. Blanked before scanning so the literal it
|
|
36
|
+
# carries is not mistaken for a real tag.
|
|
37
|
+
ESCAPED = /\{%-?\s*raw\s*-?%\}\{%-?\s*(?:raw|endraw)\s*-?%\}\{%-?\s*endraw\s*-?%\}/.freeze
|
|
38
|
+
|
|
39
|
+
# Directories Jekyll never reads, plus the ones _config.yml excludes. Kept in
|
|
40
|
+
# sync with the `exclude:` list; a path missing here only costs a false
|
|
41
|
+
# positive, never a missed one.
|
|
42
|
+
SKIP_DIRS = %w[
|
|
43
|
+
.git .github .claude .cursor .devcontainer .frontmatter .vscode .obsidian
|
|
44
|
+
.jekyll-cache .sass-cache _site node_modules vendor pkg logs
|
|
45
|
+
docs scripts templates test tests examples submodules gemfiles docker lib
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
# True when `pos` sits inside a single-backtick code span: an odd number of
|
|
49
|
+
# backtick runs precede it on its own line.
|
|
50
|
+
def in_code_span?(text, pos)
|
|
51
|
+
bol = text.rindex("\n", pos - 1)
|
|
52
|
+
line = text[(bol ? bol + 1 : 0)...pos]
|
|
53
|
+
line.scan(/`+/).size.odd?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def scan(path)
|
|
57
|
+
# Blank the escape form (keeping byte offsets and newlines) so the literal
|
|
58
|
+
# tag it renders is not read as a tag itself.
|
|
59
|
+
text = File.read(path, encoding: 'UTF-8')
|
|
60
|
+
.gsub(ESCAPED) { |m| ' ' * m.length }
|
|
61
|
+
problems = []
|
|
62
|
+
in_raw = false
|
|
63
|
+
opened_at = nil
|
|
64
|
+
opened_in_span = false
|
|
65
|
+
|
|
66
|
+
text.to_enum(:scan, TAG).each do
|
|
67
|
+
match = Regexp.last_match
|
|
68
|
+
line = text[0, match.begin(0)].count("\n") + 1
|
|
69
|
+
if in_raw
|
|
70
|
+
# Inside a raw block every tag is literal text — nothing to check.
|
|
71
|
+
if match[1] == 'endraw'
|
|
72
|
+
if opened_in_span && line != opened_at
|
|
73
|
+
problems << "#{path}:#{opened_at}: `{% raw %}` inside a code span " \
|
|
74
|
+
"opens a block that only closes at line #{line} — " \
|
|
75
|
+
'escape it to show the tag as text'
|
|
76
|
+
end
|
|
77
|
+
in_raw = false
|
|
78
|
+
opened_at = nil
|
|
79
|
+
opened_in_span = false
|
|
80
|
+
end
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if match[1] == 'raw'
|
|
85
|
+
in_raw = true
|
|
86
|
+
opened_at = line
|
|
87
|
+
# `{% raw %}` inside an inline code span is the documentation-mention
|
|
88
|
+
# trap: it reads as text but opens a real block. Legitimate inline use
|
|
89
|
+
# (`{% raw %}{{ x }}{% endraw %}`) closes on the same line; a mention
|
|
90
|
+
# does not, and swallows everything up to the next `{% endraw %}`.
|
|
91
|
+
opened_in_span = in_code_span?(text, match.begin(0))
|
|
92
|
+
else
|
|
93
|
+
problems << "#{path}:#{line}: `{% endraw %}` with no matching `{% raw %}`"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if in_raw
|
|
98
|
+
problems << "#{path}:#{opened_at}: `{% raw %}` is never closed — " \
|
|
99
|
+
'Liquid will swallow the rest of the file'
|
|
100
|
+
end
|
|
101
|
+
problems
|
|
102
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError => e
|
|
103
|
+
["#{path}: could not read as UTF-8 (#{e.class})"]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def markdown_files
|
|
107
|
+
Dir.chdir(ROOT) do
|
|
108
|
+
Dir.glob('**/*.{md,markdown}', File::FNM_DOTMATCH).reject do |p|
|
|
109
|
+
p.split(File::SEPARATOR).any? { |seg| SKIP_DIRS.include?(seg) }
|
|
110
|
+
end.sort
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
verbose = ARGV.delete('--verbose')
|
|
115
|
+
targets = ARGV.empty? ? markdown_files : ARGV
|
|
116
|
+
|
|
117
|
+
problems = []
|
|
118
|
+
Dir.chdir(ROOT) do
|
|
119
|
+
targets.each do |path|
|
|
120
|
+
next unless File.file?(path)
|
|
121
|
+
|
|
122
|
+
warn " scanning #{path}" if verbose
|
|
123
|
+
problems.concat(scan(path))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if problems.empty?
|
|
128
|
+
puts "✓ lint-liquid-raw: #{targets.size} Markdown files, all " \
|
|
129
|
+
'`{% raw %}` blocks balanced'
|
|
130
|
+
exit 0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
warn '✗ lint-liquid-raw: unbalanced Liquid raw blocks'
|
|
134
|
+
problems.each { |p| warn " #{p}" }
|
|
135
|
+
warn ''
|
|
136
|
+
warn 'To display a literal tag, escape it: `{% raw %}{% raw %}{% endraw %}`'
|
|
137
|
+
exit 1
|
|
@@ -142,15 +142,16 @@ test_mermaid_init() {
|
|
|
142
142
|
local url="$1"
|
|
143
143
|
local description="$2"
|
|
144
144
|
|
|
145
|
-
run_test "$description" "curl -s '$url' | grep -q 'mermaid.
|
|
145
|
+
run_test "$description" "curl -s '$url' | grep -q 'mermaid-diagrams.js'"
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
# Test diagram
|
|
148
|
+
# Test diagram presence (a ```mermaid fence is served as code.language-mermaid;
|
|
149
|
+
# the deferred script turns it into a figure in the browser)
|
|
149
150
|
test_diagram_rendering() {
|
|
150
151
|
local url="$1"
|
|
151
152
|
local description="$2"
|
|
152
|
-
|
|
153
|
-
run_test "$description" "curl -s '$url' | grep -q '
|
|
153
|
+
|
|
154
|
+
run_test "$description" "curl -s '$url' | grep -q 'language-mermaid'"
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
# Main test execution
|
|
@@ -179,16 +180,29 @@ main() {
|
|
|
179
180
|
# Mermaid include file tests
|
|
180
181
|
log_info "Testing Mermaid include file..."
|
|
181
182
|
|
|
182
|
-
test_file_content "_includes/components/mermaid.html" "
|
|
183
|
-
test_file_content "_includes/components/mermaid.html" "
|
|
184
|
-
test_file_content "_includes/components/mermaid.html" "
|
|
185
|
-
test_file_content "_includes/components/mermaid.html" "
|
|
183
|
+
test_file_content "_includes/components/mermaid.html" "mm.src" "Mermaid script from _config (local vendor path)"
|
|
184
|
+
test_file_content "_includes/components/mermaid.html" "mermaidConfig" "Diagram config block emitted for the behaviour script"
|
|
185
|
+
test_file_content "_includes/components/mermaid.html" "mermaid-diagrams.js" "Behaviour script loaded after the vendor bundle"
|
|
186
|
+
test_file_content "_includes/components/mermaid.html" "defer" "Scripts are deferred (never block parsing)"
|
|
187
|
+
|
|
188
|
+
# Behaviour script (assets/js/mermaid-diagrams.js)
|
|
189
|
+
log_info "Testing diagram behaviour script..."
|
|
190
|
+
|
|
191
|
+
test_file_exists "assets/js/mermaid-diagrams.js" "Diagram behaviour script exists"
|
|
192
|
+
test_file_content "assets/js/mermaid-diagrams.js" "mermaid.initialize" "Mermaid initialization"
|
|
193
|
+
test_file_content "assets/js/mermaid-diagrams.js" "data-bs-theme" "Colour-mode change observed for re-render"
|
|
194
|
+
test_file_content "assets/js/mermaid-diagrams.js" "data-theme-skin" "Skin change observed for re-render"
|
|
195
|
+
test_file_content "assets/js/mermaid-diagrams.js" "themeVariables" "Palette derived into Mermaid theme variables"
|
|
196
|
+
test_file_content "assets/js/mermaid-diagrams.js" "zer0Mermaid" "Public API exposed"
|
|
197
|
+
test_file_exists "_sass/components/_mermaid.scss" "Diagram figure styles exist"
|
|
198
|
+
test_file_content "assets/css/main.scss" "components/mermaid" "Diagram styles imported by the stylesheet manifest"
|
|
186
199
|
|
|
187
200
|
# Documentation tests
|
|
188
201
|
log_info "Testing documentation..."
|
|
189
202
|
|
|
190
203
|
test_file_content "pages/_docs/features/mermaid-diagrams.md" "mermaid: true" "Main docs have front matter"
|
|
191
204
|
test_file_content "pages/_docs/features/mermaid-diagrams.md" "graph TD" "Main docs have examples"
|
|
205
|
+
test_file_content "pages/_docs/features/mermaid-diagrams.md" "accTitle" "Main docs cover accessible captions"
|
|
192
206
|
|
|
193
207
|
# Server tests (if not quick mode)
|
|
194
208
|
if [ "$QUICK" = false ]; then
|
|
@@ -131,7 +131,9 @@ main() {
|
|
|
131
131
|
source "$TEST_DIR/test_migrate.sh"
|
|
132
132
|
source "$TEST_DIR/test_pixelate_images.sh"
|
|
133
133
|
source "$TEST_DIR/test_content_review.sh"
|
|
134
|
-
|
|
134
|
+
source "$TEST_DIR/test_agent_review_result.sh"
|
|
135
|
+
source "$TEST_DIR/test_visual_evidence_autogen.sh"
|
|
136
|
+
|
|
135
137
|
# Summary
|
|
136
138
|
echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
137
139
|
echo -e "${BLUE}Test Summary${NC}"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Bridge for scripts/ci/test_agent_review_result.py (issue #418).
|
|
4
|
+
#
|
|
5
|
+
# The guard that decides whether the Claude content review actually ran lives in
|
|
6
|
+
# scripts/ci/agent_review_result.py, next to the other CI helper
|
|
7
|
+
# (classify_changes.py), and its tests are written in Python beside it. This
|
|
8
|
+
# wrapper is what puts them on the CI path: run_tests.sh sources this file, and
|
|
9
|
+
# ./scripts/bin/test runs run_tests.sh on every PR.
|
|
10
|
+
#
|
|
11
|
+
# scripts/issues/test_verify_close.py is the cautionary example — a real test
|
|
12
|
+
# suite that no runner ever invoked. Do not let this one drift the same way: if
|
|
13
|
+
# the Python tests move, move this line with them.
|
|
14
|
+
|
|
15
|
+
ARR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
|
16
|
+
|
|
17
|
+
print_suite_header "Claude review guard (scripts/ci/agent_review_result.py)"
|
|
18
|
+
|
|
19
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
20
|
+
arr_output="$(python3 "$ARR_ROOT/scripts/ci/test_agent_review_result.py" 2>&1)"
|
|
21
|
+
arr_status=$?
|
|
22
|
+
echo "$arr_output"
|
|
23
|
+
assert_equals "0" "$arr_status" \
|
|
24
|
+
"scripts/ci/test_agent_review_result.py passes (the #418 401-swallow guard)"
|
|
25
|
+
else
|
|
26
|
+
echo "python3 not available — skipping the Claude review guard tests"
|
|
27
|
+
fi
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Bridge for scripts/ci/test_visual_evidence_autogen.py (ZER0-085).
|
|
4
|
+
#
|
|
5
|
+
# The planner/decider behind visual-evidence-autogen.yml lives in
|
|
6
|
+
# scripts/ci/visual_evidence_autogen.py, next to the other CI helpers, and its
|
|
7
|
+
# tests are Python beside it. This wrapper puts them on the CI path: run_tests.sh
|
|
8
|
+
# sources this file, and ./scripts/bin/test runs run_tests.sh on every PR — the
|
|
9
|
+
# same arrangement as test_agent_review_result.sh, for the same reason (a test
|
|
10
|
+
# suite no runner invokes rots silently).
|
|
11
|
+
|
|
12
|
+
VEA_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
|
13
|
+
|
|
14
|
+
print_suite_header "Visual-evidence autogen planner (scripts/ci/visual_evidence_autogen.py)"
|
|
15
|
+
|
|
16
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
17
|
+
vea_output="$(python3 "$VEA_ROOT/scripts/ci/test_visual_evidence_autogen.py" 2>&1)"
|
|
18
|
+
vea_status=$?
|
|
19
|
+
echo "$vea_output"
|
|
20
|
+
assert_equals "0" "$vea_status" \
|
|
21
|
+
"scripts/ci/test_visual_evidence_autogen.py passes (plan/decide/contract guards)"
|
|
22
|
+
else
|
|
23
|
+
echo "python3 not available — skipping the visual-evidence autogen tests"
|
|
24
|
+
fi
|