jekyll-theme-modootoday 1.0.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 +7 -0
- data/CHANGELOG.md +182 -0
- data/LICENSE +21 -0
- data/README.ko.md +115 -0
- data/README.md +112 -0
- data/RULES.md +120 -0
- data/_includes/footer-custom.html +5 -0
- data/_includes/head-custom.html +1 -0
- data/_includes/modootoday/actions.html +17 -0
- data/_includes/modootoday/archive-years.html +30 -0
- data/_includes/modootoday/archive.html +44 -0
- data/_includes/modootoday/blog/card.html +23 -0
- data/_includes/modootoday/blog/config.html +17 -0
- data/_includes/modootoday/blog/list.html +13 -0
- data/_includes/modootoday/blog/pager.html +15 -0
- data/_includes/modootoday/blog/reading-time.html +10 -0
- data/_includes/modootoday/blog/select.html +18 -0
- data/_includes/modootoday/brand/icon.html +39 -0
- data/_includes/modootoday/brand/logo.html +7 -0
- data/_includes/modootoday/breadcrumb.html +22 -0
- data/_includes/modootoday/config.html +41 -0
- data/_includes/modootoday/detail/cover.html +5 -0
- data/_includes/modootoday/detail/cta.html +13 -0
- data/_includes/modootoday/detail/facts.html +31 -0
- data/_includes/modootoday/detail/jsonld.html +34 -0
- data/_includes/modootoday/directory/date-tile.html +2 -0
- data/_includes/modootoday/directory/filter.html +7 -0
- data/_includes/modootoday/directory/ics.html +17 -0
- data/_includes/modootoday/directory/item.html +18 -0
- data/_includes/modootoday/directory/items.html +4 -0
- data/_includes/modootoday/directory/jsonld.html +5 -0
- data/_includes/modootoday/directory/list.html +16 -0
- data/_includes/modootoday/directory/values.html +6 -0
- data/_includes/modootoday/doc/page-actions.html +15 -0
- data/_includes/modootoday/doc/pagination.html +24 -0
- data/_includes/modootoday/doc/sidebar-item.html +11 -0
- data/_includes/modootoday/doc/sidebar.html +28 -0
- data/_includes/modootoday/doc/twin.html +8 -0
- data/_includes/modootoday/docs-extra/head.html +7 -0
- data/_includes/modootoday/docs-extra/jsonld.html +19 -0
- data/_includes/modootoday/head-meta.html +90 -0
- data/_includes/modootoday/home-url.html +4 -0
- data/_includes/modootoday/i18n/ar.html +2 -0
- data/_includes/modootoday/i18n/de.html +2 -0
- data/_includes/modootoday/i18n/el.html +2 -0
- data/_includes/modootoday/i18n/es.html +2 -0
- data/_includes/modootoday/i18n/fa.html +2 -0
- data/_includes/modootoday/i18n/fr.html +2 -0
- data/_includes/modootoday/i18n/id.html +2 -0
- data/_includes/modootoday/i18n/it.html +2 -0
- data/_includes/modootoday/i18n/ja.html +2 -0
- data/_includes/modootoday/i18n/ko.html +2 -0
- data/_includes/modootoday/i18n/ml.html +2 -0
- data/_includes/modootoday/i18n/name.html +2 -0
- data/_includes/modootoday/i18n/pl.html +2 -0
- data/_includes/modootoday/i18n/pt-BR.html +2 -0
- data/_includes/modootoday/i18n/ro.html +2 -0
- data/_includes/modootoday/i18n/ru.html +2 -0
- data/_includes/modootoday/i18n/th.html +2 -0
- data/_includes/modootoday/i18n/tr.html +2 -0
- data/_includes/modootoday/i18n/uk.html +2 -0
- data/_includes/modootoday/i18n/ur-Latn.html +2 -0
- data/_includes/modootoday/i18n/vi.html +2 -0
- data/_includes/modootoday/i18n/zh-CN.html +2 -0
- data/_includes/modootoday/i18n/zh-TW.html +2 -0
- data/_includes/modootoday/i18n.html +163 -0
- data/_includes/modootoday/jsonld/article.html +15 -0
- data/_includes/modootoday/jsonld/author.html +8 -0
- data/_includes/modootoday/jsonld/breadcrumb.html +17 -0
- data/_includes/modootoday/jsonld/entity.html +16 -0
- data/_includes/modootoday/jsonld/graph.html +24 -0
- data/_includes/modootoday/jsonld/profile.html +20 -0
- data/_includes/modootoday/jsonld/props.html +13 -0
- data/_includes/modootoday/jsonld/publisher.html +11 -0
- data/_includes/modootoday/jsonld/type.html +29 -0
- data/_includes/modootoday/jsonld/webpage.html +17 -0
- data/_includes/modootoday/jsonld/website.html +9 -0
- data/_includes/modootoday/lang-switch.html +31 -0
- data/_includes/modootoday/link.html +3 -0
- data/_includes/modootoday/links/attrs.html +9 -0
- data/_includes/modootoday/links/content.html +16 -0
- data/_includes/modootoday/markdown/c/details.html +6 -0
- data/_includes/modootoday/markdown/c/faq.html +2 -0
- data/_includes/modootoday/markdown/c/filetree.html +3 -0
- data/_includes/modootoday/markdown/c/gallery.html +5 -0
- data/_includes/modootoday/markdown/c/linkcards.html +2 -0
- data/_includes/modootoday/markdown/c/params.html +3 -0
- data/_includes/modootoday/markdown/c/pricing.html +10 -0
- data/_includes/modootoday/markdown/c/quote.html +6 -0
- data/_includes/modootoday/markdown/c/subscribe.html +6 -0
- data/_includes/modootoday/markdown/components.html +114 -0
- data/_includes/modootoday/markdown/inline.html +46 -0
- data/_includes/modootoday/markdown/llms-pages.html +14 -0
- data/_includes/modootoday/markdown/raw-body.html +6 -0
- data/_includes/modootoday/markdown/sections.html +43 -0
- data/_includes/modootoday/marketing/head.html +6 -0
- data/_includes/modootoday/meta/byline.html +24 -0
- data/_includes/modootoday/meta/post-nav.html +16 -0
- data/_includes/modootoday/meta/related.html +20 -0
- data/_includes/modootoday/meta/series.html +11 -0
- data/_includes/modootoday/meta/share.html +4 -0
- data/_includes/modootoday/nav.html +19 -0
- data/_includes/modootoday/news/badge.html +4 -0
- data/_includes/modootoday/news/front.html +30 -0
- data/_includes/modootoday/news/notes.html +10 -0
- data/_includes/modootoday/page-meta.html +16 -0
- data/_includes/modootoday/pages/error.html +13 -0
- data/_includes/modootoday/pages/hero/scene.html +27 -0
- data/_includes/modootoday/pages/hero/sparks.html +6 -0
- data/_includes/modootoday/pages/hero.html +11 -0
- data/_includes/modootoday/pages/search-form.html +14 -0
- data/_includes/modootoday/search.json +11 -0
- data/_includes/modootoday/stylesheets.html +36 -0
- data/_includes/modootoday/t.html +7 -0
- data/_includes/modootoday/theme-color.html +19 -0
- data/_includes/modootoday/toc.html +17 -0
- data/_includes/modootoday/tokens.html +11 -0
- data/_includes/sections/showcase.html +18 -0
- data/_layouts/archive.html +6 -0
- data/_layouts/author.html +19 -0
- data/_layouts/blog.html +28 -0
- data/_layouts/book.html +21 -0
- data/_layouts/brand.html +21 -0
- data/_layouts/case-study.html +5 -0
- data/_layouts/changelog.html +24 -0
- data/_layouts/contact.html +27 -0
- data/_layouts/course.html +21 -0
- data/_layouts/default.html +87 -0
- data/_layouts/detail.html +31 -0
- data/_layouts/directory.html +12 -0
- data/_layouts/doc.html +27 -0
- data/_layouts/download.html +21 -0
- data/_layouts/episode.html +5 -0
- data/_layouts/error.html +7 -0
- data/_layouts/event.html +5 -0
- data/_layouts/events.html +6 -0
- data/_layouts/feature.html +18 -0
- data/_layouts/feed.html +35 -0
- data/_layouts/glossary.html +24 -0
- data/_layouts/home.html +7 -0
- data/_layouts/hub.html +16 -0
- data/_layouts/integrations.html +7 -0
- data/_layouts/job.html +5 -0
- data/_layouts/jobs.html +7 -0
- data/_layouts/legal.html +21 -0
- data/_layouts/links.html +18 -0
- data/_layouts/llms-full.html +8 -0
- data/_layouts/llms.html +32 -0
- data/_layouts/minimal.html +8 -0
- data/_layouts/news-article.html +6 -0
- data/_layouts/news.html +22 -0
- data/_layouts/portfolio.html +7 -0
- data/_layouts/post.html +54 -0
- data/_layouts/pricing.html +32 -0
- data/_layouts/project.html +5 -0
- data/_layouts/redirect.html +21 -0
- data/_layouts/reference.html +24 -0
- data/_layouts/resume.html +50 -0
- data/_layouts/roadmap.html +22 -0
- data/_layouts/search.html +8 -0
- data/_layouts/sponsors.html +7 -0
- data/_layouts/team.html +7 -0
- data/_layouts/twin.html +2 -0
- data/_layouts/wide.html +7 -0
- data/_sass/modootoday/_access.scss +12 -0
- data/_sass/modootoday/_core.scss +18 -0
- data/_sass/modootoday/_settings.scss +5 -0
- data/_sass/modootoday/_sheet.scss +6 -0
- data/_sass/modootoday/base/_bidi.scss +8 -0
- data/_sass/modootoday/base/_code.scss +18 -0
- data/_sass/modootoday/base/_interaction.scss +6 -0
- data/_sass/modootoday/base/_prose.scss +32 -0
- data/_sass/modootoday/base/_reset.scss +14 -0
- data/_sass/modootoday/base/_table.scss +8 -0
- data/_sass/modootoday/blog/_article.scss +27 -0
- data/_sass/modootoday/blog/_cards.scss +36 -0
- data/_sass/modootoday/blog/_index.scss +10 -0
- data/_sass/modootoday/blog/_news.scss +26 -0
- data/_sass/modootoday/blog/_post.scss +9 -0
- data/_sass/modootoday/components/core/chrome/_banner.scss +7 -0
- data/_sass/modootoday/components/core/chrome/_breadcrumb.scss +10 -0
- data/_sass/modootoday/components/core/chrome/_nav.scss +39 -0
- data/_sass/modootoday/components/core/chrome/_pagination.scss +10 -0
- data/_sass/modootoday/components/core/chrome/_theme-toggle.scss +16 -0
- data/_sass/modootoday/components/core/inline/_badge.scss +15 -0
- data/_sass/modootoday/components/core/inline/_callout.scss +14 -0
- data/_sass/modootoday/components/core/inline/_code-copy.scss +11 -0
- data/_sass/modootoday/components/core/inline/_code.scss +11 -0
- data/_sass/modootoday/components/core/inline/_cta.scss +11 -0
- data/_sass/modootoday/components/core/inline/_embed.scss +6 -0
- data/_sass/modootoday/components/core/inline/_figure.scss +9 -0
- data/_sass/modootoday/components/core/inline/_footnotes.scss +6 -0
- data/_sass/modootoday/components/core/inline/_hero.scss +8 -0
- data/_sass/modootoday/components/core/inline/_kbd.scss +6 -0
- data/_sass/modootoday/components/core/inline/_progress.scss +8 -0
- data/_sass/modootoday/components/doc/_page-actions.scss +10 -0
- data/_sass/modootoday/components/doc/_sidebar.scss +31 -0
- data/_sass/modootoday/components/doc/_toc.scss +19 -0
- data/_sass/modootoday/components/loading/_loading.scss +33 -0
- data/_sass/modootoday/components/search/_search.scss +36 -0
- data/_sass/modootoday/components/section/showcase/_card.scss +15 -0
- data/_sass/modootoday/components/section/showcase/_cta.scss +7 -0
- data/_sass/modootoday/components/section/showcase/_gallery.scss +15 -0
- data/_sass/modootoday/components/section/showcase/_linkcards.scss +12 -0
- data/_sass/modootoday/components/section/showcase/_pricing.scss +12 -0
- data/_sass/modootoday/components/section/showcase/_quote.scss +7 -0
- data/_sass/modootoday/components/section/showcase/_subscribe.scss +9 -0
- data/_sass/modootoday/components/section/structure/_accordion.scss +15 -0
- data/_sass/modootoday/components/section/structure/_details.scss +15 -0
- data/_sass/modootoday/components/section/structure/_faq.scss +5 -0
- data/_sass/modootoday/components/section/structure/_filetree.scss +13 -0
- data/_sass/modootoday/components/section/structure/_params.scss +8 -0
- data/_sass/modootoday/components/section/structure/_steps.scss +13 -0
- data/_sass/modootoday/components/section/structure/_tabs.scss +21 -0
- data/_sass/modootoday/components/section/structure/_timeline.scss +10 -0
- data/_sass/modootoday/detail/_index.scss +8 -0
- data/_sass/modootoday/detail/_page.scss +25 -0
- data/_sass/modootoday/directory/_filter.scss +12 -0
- data/_sass/modootoday/directory/_grid.scss +18 -0
- data/_sass/modootoday/directory/_index.scss +11 -0
- data/_sass/modootoday/directory/_presets.scss +16 -0
- data/_sass/modootoday/docs-extra/_index.scss +7 -0
- data/_sass/modootoday/docs-extra/_page.scss +37 -0
- data/_sass/modootoday/hero/_index.scss +6 -0
- data/_sass/modootoday/hero/_layout.scss +28 -0
- data/_sass/modootoday/hero/_scene.scss +20 -0
- data/_sass/modootoday/kit/_button.scss +8 -0
- data/_sass/modootoday/layout/_page-meta.scss +9 -0
- data/_sass/modootoday/layout/_safe-area.scss +9 -0
- data/_sass/modootoday/layout/_shell.scss +56 -0
- data/_sass/modootoday/marketing/_forms.scss +11 -0
- data/_sass/modootoday/marketing/_index.scss +10 -0
- data/_sass/modootoday/marketing/_page.scss +31 -0
- data/_sass/modootoday/marketing/_pricing.scss +15 -0
- data/_sass/modootoday/pages/_actions.scss +11 -0
- data/_sass/modootoday/pages/_error.scss +7 -0
- data/_sass/modootoday/pages/_index.scss +11 -0
- data/_sass/modootoday/pages/_legal.scss +17 -0
- data/_sass/modootoday/pages/_search.scss +9 -0
- data/_sass/modootoday/pages/_shells.scss +11 -0
- data/_sass/modootoday/resume/_index.scss +5 -0
- data/_sass/modootoday/resume/_page.scss +27 -0
- data/_sass/modootoday/tokens/_emit.scss +15 -0
- data/_sass/modootoday/tokens/_primitive.scss +18 -0
- data/_sass/modootoday/tokens/_semantic.scss +37 -0
- data/_sass/modootoday/tools/_functions.scss +2 -0
- data/_sass/modootoday/tools/_img-dark.scss +12 -0
- data/_sass/modootoday/tools/_mixins.scss +8 -0
- data/_sass/modootoday.scss +40 -0
- data/assets/css/modootoday/doc.scss +10 -0
- data/assets/css/modootoday/loading.scss +8 -0
- data/assets/css/modootoday/search.scss +8 -0
- data/assets/css/modootoday/showcase/card.scss +7 -0
- data/assets/css/modootoday/showcase/cta.scss +7 -0
- data/assets/css/modootoday/showcase/gallery.scss +7 -0
- data/assets/css/modootoday/showcase/linkcards.scss +7 -0
- data/assets/css/modootoday/showcase/pricing.scss +7 -0
- data/assets/css/modootoday/showcase/quote.scss +7 -0
- data/assets/css/modootoday/showcase/subscribe.scss +7 -0
- data/assets/css/modootoday/structure/accordion.scss +7 -0
- data/assets/css/modootoday/structure/details.scss +7 -0
- data/assets/css/modootoday/structure/faq.scss +7 -0
- data/assets/css/modootoday/structure/filetree.scss +7 -0
- data/assets/css/modootoday/structure/params.scss +7 -0
- data/assets/css/modootoday/structure/steps.scss +7 -0
- data/assets/css/modootoday/structure/tabs.scss +7 -0
- data/assets/css/modootoday/structure/timeline.scss +7 -0
- data/assets/css/modootoday-blog.scss +7 -0
- data/assets/css/modootoday-detail.scss +7 -0
- data/assets/css/modootoday-directory.scss +7 -0
- data/assets/css/modootoday-docs-extra.scss +7 -0
- data/assets/css/modootoday-hero.scss +7 -0
- data/assets/css/modootoday-marketing.scss +7 -0
- data/assets/css/modootoday-pages.scss +7 -0
- data/assets/css/modootoday-resume.scss +7 -0
- data/assets/css/modootoday.scss +17 -0
- data/assets/js/modootoday/accordion.js +13 -0
- data/assets/js/modootoday/code-copy.js +29 -0
- data/assets/js/modootoday/menu.js +19 -0
- data/assets/js/modootoday/scroll-region.js +14 -0
- data/assets/js/modootoday/search-keys.js +18 -0
- data/assets/js/modootoday/search.js +31 -0
- data/assets/js/modootoday/share.js +16 -0
- data/assets/js/modootoday/tabs.js +26 -0
- data/assets/js/modootoday/theme-init.js +2 -0
- data/assets/js/modootoday/theme-toggle.js +17 -0
- data/assets/modootoday/apple-touch-icon.png +0 -0
- data/assets/modootoday/favicon-32.png +0 -0
- data/assets/modootoday/favicon.svg +1 -0
- data/assets/modootoday/hero/dawn-1280.webp +0 -0
- data/assets/modootoday/hero/dawn-640.webp +0 -0
- data/assets/modootoday/hero/dawn-960.webp +0 -0
- data/exe/modootoday-check +7 -0
- data/i18n/ar.yml +130 -0
- data/i18n/de.yml +130 -0
- data/i18n/el.yml +130 -0
- data/i18n/en.yml +131 -0
- data/i18n/es.yml +130 -0
- data/i18n/fa.yml +130 -0
- data/i18n/fr.yml +130 -0
- data/i18n/glossary.yml +61 -0
- data/i18n/id.yml +130 -0
- data/i18n/it.yml +130 -0
- data/i18n/ja.yml +130 -0
- data/i18n/ko.yml +130 -0
- data/i18n/languages.yml +25 -0
- data/i18n/ml.yml +130 -0
- data/i18n/pl.yml +130 -0
- data/i18n/pt-BR.yml +130 -0
- data/i18n/ro.yml +130 -0
- data/i18n/ru.yml +130 -0
- data/i18n/th.yml +130 -0
- data/i18n/tr.yml +130 -0
- data/i18n/uk.yml +130 -0
- data/i18n/ur-Latn.yml +130 -0
- data/i18n/vi.yml +130 -0
- data/i18n/zh-CN.yml +130 -0
- data/i18n/zh-TW.yml +130 -0
- data/lib/jekyll-theme-modootoday.rb +59 -0
- data/lib/modootoday/check/a11y/content.rb +57 -0
- data/lib/modootoday/check/a11y/new_window.rb +64 -0
- data/lib/modootoday/check/blog.rb +72 -0
- data/lib/modootoday/check/build_output.rb +39 -0
- data/lib/modootoday/check/built_parts.rb +40 -0
- data/lib/modootoday/check/cli.rb +61 -0
- data/lib/modootoday/check/component_rules.rb +54 -0
- data/lib/modootoday/check/context.rb +41 -0
- data/lib/modootoday/check/gh_render.rb +21 -0
- data/lib/modootoday/check/github_pages.rb +23 -0
- data/lib/modootoday/check/i18n.rb +92 -0
- data/lib/modootoday/check/layouts/detail.rb +52 -0
- data/lib/modootoday/check/layouts/directory.rb +36 -0
- data/lib/modootoday/check/layouts/docs_extra.rb +84 -0
- data/lib/modootoday/check/layouts/marketing.rb +64 -0
- data/lib/modootoday/check/layouts/shape.rb +38 -0
- data/lib/modootoday/check/layouts/shells.rb +47 -0
- data/lib/modootoday/check/layouts.rb +37 -0
- data/lib/modootoday/check/markdown.rb +39 -0
- data/lib/modootoday/check/page_sections.rb +72 -0
- data/lib/modootoday/check/page_syntax.rb +104 -0
- data/lib/modootoday/check/page_values.rb +30 -0
- data/lib/modootoday/check/pages.rb +31 -0
- data/lib/modootoday/check/registry.rb +69 -0
- data/lib/modootoday/check/site_config.rb +74 -0
- data/lib/modootoday/check/slots.rb +50 -0
- data/lib/modootoday/check/structured_data.rb +97 -0
- data/lib/modootoday/check/stylesheets.rb +28 -0
- data/lib/modootoday/check/twins.rb +69 -0
- data/lib/modootoday/check.rb +22 -0
- data/lib/modootoday/schema/rules.rb +49 -0
- data/lib/modootoday/schema/vocab.txt +2478 -0
- metadata +420 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# Rules 0-1: front matter present, fence info strings, alerts, forbidden syntax, section headings.
|
|
6
|
+
module PageSyntax
|
|
7
|
+
KNOWN = %w[NOTE TIP IMPORTANT WARNING CAUTION].freeze
|
|
8
|
+
NESTED_ALERT = "alert nested in list/table/quote (GitHub renders only top-level alerts; this shows as a plain quote); start '> [!TYPE]' at column 0"
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Returns the number of top-level '## ' headings (used by the sections: rule).
|
|
13
|
+
def call(ctx, f, text, b, code)
|
|
14
|
+
front_matter(ctx, f, text)
|
|
15
|
+
fences(ctx, f, b)
|
|
16
|
+
alerts(ctx, f, code.lines)
|
|
17
|
+
html_free = Markdown.strip_comments(code)
|
|
18
|
+
forbidden(ctx, f, code, html_free)
|
|
19
|
+
headings(ctx, f, html_free)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# 0. every content page needs front matter (may be empty "---\n---"); without it,
|
|
23
|
+
# GitHub Pages (jekyll-optional-front-matter) renders the page AND copies the raw .md to _site.
|
|
24
|
+
def front_matter(ctx, f, text)
|
|
25
|
+
ctx.errs << "#{ctx.rel(f)}: missing front matter (add an empty '---' / '---' block at the top)" unless (text.start_with?("---\n") && text =~ Markdown::FM) || ctx.source_mode?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 1a. code fence info string: one language word only (kramdown 2.4 turns "sh {x}" into inline code)
|
|
29
|
+
def fences(ctx, f, b)
|
|
30
|
+
Markdown.strip_fences(b) do |n, info|
|
|
31
|
+
next if info.empty? || info.match?(/\A[\w#+.-]+\z/)
|
|
32
|
+
ctx.errs << "#{ctx.rel(f)}:#{n}: code fence info string must be a single language word (got #{info.inspect}); attributes like {title=…} break kramdown"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 1b. alerts (GitHub extension): case-insensitive markers, registered types only
|
|
37
|
+
def alerts(ctx, f, lines)
|
|
38
|
+
errs, r = ctx.errs, ctx.rel(f)
|
|
39
|
+
lines.each_with_index do |l, i|
|
|
40
|
+
n = i + 1
|
|
41
|
+
errs << "#{r}:#{n}: tab after '>' (kramdown reads it as an indented code block); use a space" if l =~ /^ {0,3}>\t/
|
|
42
|
+
errs << "#{r}:#{n}: formatted alert marker (e.g. **[!NOTE]**) is not an alert anywhere; write > [!NOTE]" if l =~ /^ {0,3}> ?\*{1,2}\[!\w+\]/
|
|
43
|
+
ctx.warns << "#{r}:#{n}: '[! TYPE]' with a space is not an alert (renders as a plain quote)" if l =~ /^ {0,3}> ?\[! +\w+\]/
|
|
44
|
+
next unless (m = l.match(/^ {0,3}> ?\[!(\w+)\](.*)$/))
|
|
45
|
+
t, after = m[1], m[2]
|
|
46
|
+
next errs << "#{r}:#{n}: unregistered alert type [!#{t}] (allowed: #{KNOWN.join('/')}; any case)" unless KNOWN.include?(t.upcase)
|
|
47
|
+
errs << "#{r}:#{n}: text after [!#{t}] (custom titles are not supported; GitHub shows a plain quote)" unless after.strip.empty?
|
|
48
|
+
rest = lines[(i + 1)..].to_a.take_while { |x| x.strip != "" }
|
|
49
|
+
errs << "#{r}:#{n}: empty alert [!#{t}] (GitHub shows a plain quote); add body text" if rest.all? { |x| x.sub(/^ {0,3}>/, "").strip.empty? }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Tool-generated region markers on their own line, e.g. <!-- spec:toc:begin --> / <!-- spec:toc:end -->
|
|
54
|
+
MARKER = /^[ \t]*<!--[ \t]*[a-z][a-z0-9-]*(?::[a-z0-9._-]+)*:(?:begin|end)[ \t]*-->[ \t]*$/
|
|
55
|
+
|
|
56
|
+
# 1c. forbidden syntax outside code
|
|
57
|
+
def forbidden(ctx, f, code, html_free)
|
|
58
|
+
r = ctx.rel(f)
|
|
59
|
+
ctx.errs << "#{r}: HTML comment <!-- --> (hidden on GitHub, emitted by kramdown); remove it" if code.gsub(MARKER, "").include?("<!--")
|
|
60
|
+
html_free.each_line.with_index(1) do |l, n|
|
|
61
|
+
linkless = l.gsub(/\]\([^)]*\)/, "]()").gsub(/<(https?:|mailto:)[^>]*>|<[^@\s>]+@[^>\s]+>/, "<>").sub(/^ {0,3}\[[^\]]+\]:\s*\S+/, "")
|
|
62
|
+
why = case l
|
|
63
|
+
when /<\/?[a-zA-Z][\w-]*[\s>\/]/ then "raw HTML (not portable: GitHub renders some tags, strips others; kramdown passes all through)"
|
|
64
|
+
when /\{%|\{\{/ then "Liquid"
|
|
65
|
+
when /\{:/ then "kramdown IAL/ALD"
|
|
66
|
+
when /\{#[\w-]+\}/ then "{#id} header id"
|
|
67
|
+
when /^:\s/ then "definition list"
|
|
68
|
+
when /^\*\[[^\]]+\]:/ then "abbreviation definition *[X]: (kramdown-only)"
|
|
69
|
+
when /^\s*([-*+]|\d+\.|\|).*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/i, /^\s*(>\s*){2,}\[!/ then NESTED_ALERT
|
|
70
|
+
end
|
|
71
|
+
why ||= "bare URL/e-mail autolink (kramdown does not link it); write <https://…> or [text](url)" if linkless =~ %r{(?<![<\w/@.])(https?://|www\.)\S|(?<![<\w.+-])[\w.+-]+@[\w-]+\.[a-z]{2,}}i
|
|
72
|
+
ctx.errs << "#{r}:#{n}: #{why}" if why
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Section headings: only column-0 '## ' starts a section; returns how many there are.
|
|
77
|
+
def headings(ctx, f, html_free)
|
|
78
|
+
h2 = 0
|
|
79
|
+
prev = ""
|
|
80
|
+
list = false # inside a list item (last column-0 block started with a list marker)
|
|
81
|
+
html_free.each_line.with_index(1) do |l, n|
|
|
82
|
+
list = l =~ /^([-*+]|\d+[.)])\s/ ? true : false if l =~ /^\S/
|
|
83
|
+
msg = case l
|
|
84
|
+
when /^##( |$)/ then (h2 += 1; nil)
|
|
85
|
+
when /^\s+##( |$)/
|
|
86
|
+
if list then "'##' inside a list item is not a section (sections split only on column-0 '## '); use '###' or move it out of the list"
|
|
87
|
+
elsif l =~ /^ {1,3}#/ then "indented '##' (kramdown: paragraph, GitHub: heading); start at column 0"
|
|
88
|
+
end
|
|
89
|
+
when /^\s+> ?\[!\w+\]/ then list ? NESTED_ALERT : nil
|
|
90
|
+
when /^\s*(>\s*)+##( |$)/ then "'##' inside a quote/alert is not a section (sections split only on column-0 '## '; GitHub would show an h2 inside the box); use '###' or bold text"
|
|
91
|
+
when /^ {0,3}-+\s*$/
|
|
92
|
+
if prev =~ /\S/ && prev !~ /^\s*([-*+>|#]|\d+\.)/
|
|
93
|
+
n -= 1
|
|
94
|
+
"setext heading (text underlined with ---) is not allowed; use '## '"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
ctx.errs << "#{ctx.rel(f)}:#{n}: #{msg}" if msg
|
|
98
|
+
prev = l
|
|
99
|
+
end
|
|
100
|
+
h2
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# Rule 2: badge vocabularies in code spans (status:, tag:, kbd:; since: is rejected before 1.1) and image alt text.
|
|
6
|
+
module PageValues
|
|
7
|
+
STATUS = %w[available partial planned].freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def call(ctx, f, b)
|
|
12
|
+
errs, r = ctx.errs, ctx.rel(f)
|
|
13
|
+
text = Markdown.strip_fences(b)
|
|
14
|
+
text.scan(/`status:([^`\s]*)`/) do |(v)|
|
|
15
|
+
errs << "#{r}: unregistered status value `status:#{v}` (allowed: #{STATUS.join('/')})" unless STATUS.include?(v)
|
|
16
|
+
end
|
|
17
|
+
text.scan(/`tag:([^`]*)`/) do |(v)|
|
|
18
|
+
errs << "#{r}: `tag:#{v}` badge value must be one token [A-Za-z0-9._-]" unless v =~ /\A[A-Za-z0-9._-]+\z/
|
|
19
|
+
end
|
|
20
|
+
text.scan(/`since:([^`]*)`/) do |(v)|
|
|
21
|
+
errs << "#{r}: `since:#{v}` is not supported before 1.1 (the API is not released yet); remove it"
|
|
22
|
+
end
|
|
23
|
+
text.scan(/`kbd:([^`]*)`/) do |(v)|
|
|
24
|
+
errs << "#{r}: `kbd:#{v}` must be keys joined by '+' without spaces (e.g. `kbd:Ctrl+K`)" unless v =~ /\A[^\s+]+(\+[^\s+]+)*\z/
|
|
25
|
+
end
|
|
26
|
+
errs << "#{r}: image without alt text (); write " if text.gsub(/`[^`\n]*`/, "") =~ /!\[\s*\]\(/
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "page_syntax"
|
|
4
|
+
require_relative "page_values"
|
|
5
|
+
require_relative "component_rules"
|
|
6
|
+
require_relative "page_sections"
|
|
7
|
+
require_relative "a11y/content"
|
|
8
|
+
require_relative "a11y/new_window"
|
|
9
|
+
|
|
10
|
+
module Modootoday
|
|
11
|
+
module Check
|
|
12
|
+
# Per-page Markdown rules, run once per content page in this order.
|
|
13
|
+
module Pages
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def call(ctx)
|
|
17
|
+
ctx.mds.each do |f|
|
|
18
|
+
text = File.read(f)
|
|
19
|
+
b = Markdown.body(text)
|
|
20
|
+
code = Markdown.strip_code(b)
|
|
21
|
+
h2 = PageSyntax.call(ctx, f, text, b, code)
|
|
22
|
+
PageValues.call(ctx, f, b)
|
|
23
|
+
ComponentRules.inline(ctx, ctx.rel(f), b)
|
|
24
|
+
PageSections.call(ctx, f, text, b, code, h2)
|
|
25
|
+
A11yContent.call(ctx, f, Markdown.strip_comments(code))
|
|
26
|
+
NewWindow.call(ctx, f, text, Markdown.strip_comments(code))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "slots"
|
|
4
|
+
|
|
5
|
+
module Modootoday
|
|
6
|
+
module Check
|
|
7
|
+
# _data/components.yml: the registry's own shape (keys, slot rules, parts against api.md)
|
|
8
|
+
# and the i18n keys it declares.
|
|
9
|
+
module Registry
|
|
10
|
+
REG_KEYS = %w[params i18n slots parts].freeze
|
|
11
|
+
RULE_KEYS = %w[required min max].freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def call(ctx)
|
|
16
|
+
errs = ctx.errs
|
|
17
|
+
api_md = "#{ctx.src}/api.md"
|
|
18
|
+
api_codes = File.exist?(api_md) && (tbl = File.read(api_md)[/^## Component structure\n.*?(?=^## )/m]) ? tbl.scan(/`([^`]+)`/).flatten.map { |x| x.gsub("\\|", "|") } : nil
|
|
19
|
+
(ctx.comps.is_a?(Hash) ? ctx.comps : {}).each do |c, spec|
|
|
20
|
+
where = "_data/components.yml: #{c}"
|
|
21
|
+
next if spec.nil?
|
|
22
|
+
next errs << "#{where} must be a map (#{REG_KEYS.join(', ')})" unless spec.is_a?(Hash)
|
|
23
|
+
(spec.keys - REG_KEYS).each { |k| errs << "#{where}: unknown key '#{k}' (known: #{REG_KEYS.join(', ')})" }
|
|
24
|
+
slots(errs, where, spec["slots"]) if spec.key?("slots")
|
|
25
|
+
errs << "#{where}.params must be a map of param name -> {allowed, required}" if spec.key?("params") && !spec["params"].is_a?(Hash)
|
|
26
|
+
parts(errs, where, spec["parts"], api_codes) if spec.key?("parts")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def slots(errs, where, slots)
|
|
31
|
+
forms = slots.is_a?(Array) ? slots : [slots]
|
|
32
|
+
errs << "#{where}.slots: a list of alternative forms needs at least 2 maps" if slots.is_a?(Array) && forms.size < 2
|
|
33
|
+
forms.each_with_index do |form, fi|
|
|
34
|
+
fw = slots.is_a?(Array) ? "#{where}.slots[#{fi}]" : "#{where}.slots"
|
|
35
|
+
next errs << "#{fw} must be a map of slot name -> {required, min, max}" unless form.is_a?(Hash)
|
|
36
|
+
form.each do |name, rule|
|
|
37
|
+
next errs << "#{fw}: unknown slot '#{name}' (known: #{Slots.names.join(', ')})" unless Slots.known?(name)
|
|
38
|
+
next if rule.nil?
|
|
39
|
+
next errs << "#{fw}.#{name} must be a map of required, min, max" unless rule.is_a?(Hash)
|
|
40
|
+
(rule.keys - RULE_KEYS).each { |k| errs << "#{fw}.#{name}: unknown key '#{k}' (known: #{RULE_KEYS.join(', ')})" }
|
|
41
|
+
errs << "#{fw}.#{name}.required must be true or false" if rule.key?("required") && ![true, false].include?(rule["required"])
|
|
42
|
+
%w[min max].each { |k| errs << "#{fw}.#{name}.#{k} must be a whole number >= 0" if rule.key?(k) && !(rule[k].is_a?(Integer) && rule[k] >= 0) }
|
|
43
|
+
mn = rule["min"].is_a?(Integer) ? rule["min"] : (rule["required"] == true ? 1 : nil)
|
|
44
|
+
errs << "#{fw}.#{name}: min #{mn} is greater than max #{rule['max']}" if mn && rule["max"].is_a?(Integer) && mn > rule["max"]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parts(errs, where, parts, api_codes)
|
|
50
|
+
return errs << "#{where}.parts must be a list of CSS selectors" unless parts.is_a?(Array) && parts.all? { |x| x.is_a?(String) && !x.strip.empty? }
|
|
51
|
+
parts.each do |sel|
|
|
52
|
+
errs << "#{where}.parts: '#{sel}' does not look like a CSS selector" unless sel.match?(/\A[\w\s.#\[\]=|"'>+~:()*-]+\z/)
|
|
53
|
+
errs << "#{where}.parts: '#{sel}' is not in the Component structure table of api.md" if api_codes && !api_codes.include?(sel)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Every i18n key a component declares exists in every _data/i18n/<lang>.yml or the theme catalog for that language.
|
|
58
|
+
def i18n_keys(ctx)
|
|
59
|
+
return unless ctx.comps
|
|
60
|
+
dig = ->(h, key) { key.split(".").reduce(h) { |a, k| a.is_a?(Hash) ? a[k] : nil } }
|
|
61
|
+
ctx.comps.each do |name, spec|
|
|
62
|
+
Array(spec && spec["i18n"]).each do |key|
|
|
63
|
+
ctx.i18n.each { |lang, h| ctx.errs << "_data/i18n/#{lang}.yml: missing key '#{key}' (component #{name})" if dig[h, key].nil? && !I18n.theme_keys(lang).include?(key) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# The modootoday: settings in the site's _config.yml.
|
|
6
|
+
module SiteConfig
|
|
7
|
+
COMPONENTS = %w[callout badge hero cta card tabs code-copy nav sidebar page-actions pagination theme-toggle breadcrumb toc accordion steps figure timeline kbd search code footnotes progress embed banner filetree details faq params linkcards quote gallery pricing subscribe loading].freeze
|
|
8
|
+
SEMANTIC = %w[background foreground muted muted-foreground card card-foreground primary primary-foreground brand accent
|
|
9
|
+
accent-foreground border ring note tip important warning caution status-available status-partial
|
|
10
|
+
status-planned font-sans font-mono font-size line-height measure space radius].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# twins: source needs Jekyll's raw copies of front-matter-less pages.
|
|
15
|
+
def source_mode(ctx)
|
|
16
|
+
return unless ctx.source_mode?
|
|
17
|
+
cfg = ctx.site_cfg
|
|
18
|
+
of = cfg["optional_front_matter"] || {}
|
|
19
|
+
ctx.errs << "_config.yml: modootoday.twins: source needs the raw copies; remove optional_front_matter.remove_originals" if of.is_a?(Hash) && of["remove_originals"] == true
|
|
20
|
+
ctx.errs << "_config.yml: modootoday.twins: source conflicts with require_front_matter: true" if cfg["require_front_matter"] == true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(ctx)
|
|
24
|
+
mtc, errs = ctx.mtc, ctx.errs
|
|
25
|
+
return errs << "_config.yml: modootoday: must be a map" if mtc && !mtc.is_a?(Hash)
|
|
26
|
+
return unless mtc.is_a?(Hash)
|
|
27
|
+
enums(mtc, errs)
|
|
28
|
+
if mtc.key?("archive")
|
|
29
|
+
a = mtc["archive"]
|
|
30
|
+
if !a.is_a?(Hash) || !(a.keys - %w[tags categories]).empty?
|
|
31
|
+
errs << "_config.yml: modootoday.archive must be a map with tags: and/or categories: (archive page URLs)"
|
|
32
|
+
else
|
|
33
|
+
a.each { |k, v| errs << "_config.yml: modootoday.archive.#{k} must be a site path like /#{k}/ (starts and ends with /)" unless v.to_s =~ %r{\A/(\S*/)?\z} }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
errs << "_config.yml: modootoday.edit_url must be an http(s) URL ending in / (page.path is appended)" if mtc.key?("edit_url") && mtc["edit_url"].to_s !~ %r{\Ahttps?://\S+/\z}
|
|
37
|
+
ctx.warns << "_config.yml: modootoday.feed: true links /feed.xml; add jekyll-feed to plugins" if mtc["feed"] == true && !Array(ctx.site_cfg["plugins"]).map(&:to_s).include?("jekyll-feed")
|
|
38
|
+
%w[css_layers badges seo hreflang feed page_actions back_to_top loading].each { |k| errs << "_config.yml: modootoday.#{k} must be true or false" if mtc.key?(k) && ![true, false].include?(mtc[k]) }
|
|
39
|
+
errs << "_config.yml: modootoday.css must be split or bundle" if mtc.key?("css") && !%w[split bundle].include?(mtc["css"])
|
|
40
|
+
errs << "_config.yml: modootoday.banner must be a map with text: (and optional url:, link:)" if mtc.key?("banner") && !(mtc["banner"].is_a?(Hash) && mtc["banner"]["text"])
|
|
41
|
+
errs << "_config.yml: modootoday.sidebar must be auto, true or false" if mtc.key?("sidebar") && ![true, false, "auto"].include?(mtc["sidebar"])
|
|
42
|
+
if mtc.key?("components")
|
|
43
|
+
list = mtc["components"].is_a?(Array) ? mtc["components"] : (errs << "_config.yml: modootoday.components must be a list"; [])
|
|
44
|
+
(list.map(&:to_s) - COMPONENTS).each { |c| errs << "_config.yml: modootoday.components: unknown component '#{c}' (known: #{COMPONENTS.join(', ')})" }
|
|
45
|
+
end
|
|
46
|
+
tokens(ctx)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def enums(mtc, errs)
|
|
50
|
+
errs << "_config.yml: modootoday.twins must be true, false or source (got #{mtc['twins'].inspect})" if mtc.key?("twins") && ![true, false, "source"].include?(mtc["twins"])
|
|
51
|
+
errs << "_config.yml: modootoday.color_scheme must be auto, auto-dark, auto-light, light or dark (got #{mtc['color_scheme'].inspect})" if mtc.key?("color_scheme") && !%w[auto auto-dark auto-light light dark].include?(mtc["color_scheme"])
|
|
52
|
+
errs << "_config.yml: modootoday.csp must be strict or false (got #{mtc['csp'].inspect})" if mtc.key?("csp") && mtc["csp"] != "strict" && mtc["csp"] != false
|
|
53
|
+
errs << "_config.yml: modootoday.languages must be a list of language codes such as [en, ko]" if mtc.key?("languages") && !(mtc["languages"].is_a?(Array) && mtc["languages"].all? { |l| l.to_s =~ /\A[a-z]{2,3}(-[A-Za-z0-9]+)*\z/ })
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def tokens(ctx)
|
|
57
|
+
mtc, errs = ctx.mtc, ctx.errs
|
|
58
|
+
%w[tokens tokens_dark].each do |k|
|
|
59
|
+
next unless mtc.key?(k)
|
|
60
|
+
next errs << "_config.yml: modootoday.#{k} must be a map" unless mtc[k].is_a?(Hash)
|
|
61
|
+
mtc[k].each do |name, val|
|
|
62
|
+
ctx.warns << "_config.yml: modootoday.#{k}.#{name}: not a semantic token (#{SEMANTIC.join(', ')})" unless SEMANTIC.include?(name.to_s)
|
|
63
|
+
v = val.to_s.strip
|
|
64
|
+
if v.empty? || v =~ /[;{}<>]/ || v !~ /\A[#\w\s.,%()"'+\-*\/]+\z/
|
|
65
|
+
errs << "_config.yml: modootoday.#{k}.#{name}: invalid CSS value #{val.inspect}"
|
|
66
|
+
elsif v.start_with?("#") && v !~ /\A#(\h{3}|\h{4}|\h{6}|\h{8})\z/
|
|
67
|
+
errs << "_config.yml: modootoday.#{k}.#{name}: invalid hex color #{val.inspect}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# Slot contract: named, stable parts of a section's Markdown. Detection is structural and
|
|
6
|
+
# mirrors what modootoday/markdown/sections.html and the component scripts consume; markup does not change.
|
|
7
|
+
module Slots
|
|
8
|
+
DETECT = {
|
|
9
|
+
"lead" => ->(m) { m.split(/\n\s*\n/).reject { |p| p.strip.empty? }.take_while { |p| p !~ /\A\s*([-*+]\s|\d+[.)]\s|###|```|~~~|>)/ }.count { |p| p =~ /\S/ && p !~ /\A\s*(\[[^\]]+\]\([^)]*\)\s*)+\z/ } },
|
|
10
|
+
"items" => ->(m) { m.scan(/^([-*+]|\d+[.)])\s/).size },
|
|
11
|
+
"panels" => ->(m) { m.scan(/^###(?: |$)/).size },
|
|
12
|
+
"actions" => ->(m) { m.scan(/^(?:\[[^\]\n]+\]\([^)\n]*\)[ \t]*)+$/).size },
|
|
13
|
+
"code" => ->(_m, raw) { n = 0; Markdown.strip_fences(raw) { n += 1 }; n },
|
|
14
|
+
"rows" => ->(m) { [m.scan(/^\s*\|.*\|\s*$/).size - 2, 0].max },
|
|
15
|
+
"quotes" => ->(m) { m.split(/\n\s*\n/).count { |p| p =~ /\A\s*>/ } },
|
|
16
|
+
"figures" => ->(m) { m.scan(/^!\[[^\]\n]+\]\([^)\n]+\)/).size },
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def known?(name) = DETECT.key?(name)
|
|
22
|
+
def names = DETECT.keys
|
|
23
|
+
|
|
24
|
+
# The problem with one slot of one section, or nil.
|
|
25
|
+
def count_problem(name, rule, md)
|
|
26
|
+
m, raw = md || ["", ""]
|
|
27
|
+
det = DETECT[name]
|
|
28
|
+
n = det.arity == 2 ? det.call(m, raw) : det.call(m)
|
|
29
|
+
rule = {} unless rule.is_a?(Hash)
|
|
30
|
+
min = rule["min"].is_a?(Integer) ? rule["min"] : (rule["required"] == true ? 1 : nil)
|
|
31
|
+
max = rule["max"].is_a?(Integer) ? rule["max"] : nil
|
|
32
|
+
return "slot '#{name}' has #{n} item(s), needs at least #{min}" if min && n < min
|
|
33
|
+
"slot '#{name}' has #{n} item(s), allows at most #{max}" if max && n > max
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# slots: is a map (one form) or a list of maps (alternative forms; a section must satisfy one).
|
|
37
|
+
def check_section(errs, file, i, c, slots, md)
|
|
38
|
+
return if slots.nil?
|
|
39
|
+
forms = (slots.is_a?(Array) ? slots : [slots]).select { |f| f.is_a?(Hash) }
|
|
40
|
+
probs = forms.map { |form| form.select { |name, _| known?(name) }.filter_map { |name, rule| count_problem(name, rule, md) } }
|
|
41
|
+
return if probs.empty? || probs.any?(&:empty?)
|
|
42
|
+
if slots.is_a?(Array)
|
|
43
|
+
errs << "#{file}: sections[#{i}] #{c}: matches none of #{forms.size} slot forms (#{probs.each_with_index.map { |p, k| "form #{k + 1}: #{p.join(', ')}" }.join('; ')})"
|
|
44
|
+
else
|
|
45
|
+
probs[0].each { |p| errs << "#{file}: sections[#{i}] #{c}: #{p}" }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../schema/rules"
|
|
5
|
+
|
|
6
|
+
module Modootoday
|
|
7
|
+
module Check
|
|
8
|
+
# 6. Built site: schema.org JSON-LD. At most one application/ld+json block per page; valid JSON with the
|
|
9
|
+
# schema.org @context; every @type and property in the vocabulary; required properties per type
|
|
10
|
+
# (recommended ones are warnings with --schema-recommended); absolute URLs; ISO 8601 dates; unique @id; the page node agrees
|
|
11
|
+
# with og:url, og:title and og:image; inLanguage matches <html lang>.
|
|
12
|
+
module StructuredData
|
|
13
|
+
BLOCK = %r{<script type="application/ld\+json">(.*?)</script>}m
|
|
14
|
+
CONTEXT = %r{\Ahttps?://schema\.org/?\z}
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def call(ctx)
|
|
19
|
+
Dir.glob("#{ctx.site}/**/*.html").sort.each { |f| page(ctx, f, File.read(f)) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def page(ctx, f, html)
|
|
23
|
+
blocks = html.scan(BLOCK).flatten
|
|
24
|
+
return if blocks.empty?
|
|
25
|
+
|
|
26
|
+
ctx.errs << "#{f}: #{blocks.size} JSON-LD blocks (use one @graph per page)" if blocks.size > 1
|
|
27
|
+
blocks.each do |b|
|
|
28
|
+
data = begin
|
|
29
|
+
JSON.parse(b)
|
|
30
|
+
rescue JSON::ParserError => e
|
|
31
|
+
next ctx.errs << "#{f}: JSON-LD is not valid JSON (#{e.message[0, 80]})"
|
|
32
|
+
end
|
|
33
|
+
ctx.errs << "#{f}: JSON-LD @context is not https://schema.org" unless data["@context"].to_s =~ CONTEXT
|
|
34
|
+
nodes = data.key?("@graph") ? Array(data["@graph"]) : [data]
|
|
35
|
+
ids = nodes.filter_map { |n| n["@id"] if n.is_a?(Hash) }
|
|
36
|
+
ids.tally.each { |id, c| ctx.errs << "#{f}: JSON-LD @id #{id} used by #{c} nodes" if c > 1 }
|
|
37
|
+
nodes.each { |n| node(ctx, f, n, "@graph") }
|
|
38
|
+
meta(ctx, f, html, nodes)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A node, then its nested nodes. Pure references ({"@id": ...}) need no @type.
|
|
43
|
+
def node(ctx, f, n, path)
|
|
44
|
+
return unless n.is_a?(Hash)
|
|
45
|
+
|
|
46
|
+
types = Array(n["@type"])
|
|
47
|
+
if types.empty?
|
|
48
|
+
ctx.errs << "#{f}: JSON-LD #{path} has no @type" unless n.keys == ["@id"] || path.end_with?("]")
|
|
49
|
+
end
|
|
50
|
+
types.each { |t| ctx.errs << "#{f}: JSON-LD @type #{t} is not in the schema.org vocabulary" unless Schema.type?(t) }
|
|
51
|
+
types.each { |t| required(ctx, f, n, t, path) }
|
|
52
|
+
n.each do |k, v|
|
|
53
|
+
next if %w[@context @type @graph].include?(k)
|
|
54
|
+
|
|
55
|
+
ctx.errs << "#{f}: JSON-LD #{path}.#{k} is not a schema.org property" unless k.start_with?("@") || Schema.property?(k)
|
|
56
|
+
values(ctx, f, k, v, "#{path}.#{k}")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def values(ctx, f, k, v, path)
|
|
61
|
+
Array(v).each_with_index do |x, i|
|
|
62
|
+
next node(ctx, f, x, v.is_a?(Array) ? "#{path}[#{i}]" : path) if x.is_a?(Hash)
|
|
63
|
+
next unless x.is_a?(String)
|
|
64
|
+
|
|
65
|
+
ctx.errs << "#{f}: JSON-LD #{path} is not an absolute URL (#{x[0, 60]})" if Schema::URL_KEYS.include?(k) && x !~ %r{\Ahttps?://}
|
|
66
|
+
ctx.errs << "#{f}: JSON-LD #{path} is not an ISO 8601 date (#{x[0, 40]})" if Schema::DATE_KEYS.include?(k) && x !~ Schema::ISO_8601
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def required(ctx, f, n, type, path)
|
|
71
|
+
req, rec = Schema::RULES[type]
|
|
72
|
+
return unless req
|
|
73
|
+
|
|
74
|
+
has = ->(alt) { alt.split("|").any? { |p| !n[p].nil? && n[p] != "" && n[p] != [] } }
|
|
75
|
+
req.each { |p| ctx.errs << "#{f}: JSON-LD #{type} (#{path}) lacks required #{p.tr('|', '/')}" unless has.(p) }
|
|
76
|
+
return unless ctx.option?("--schema-recommended")
|
|
77
|
+
|
|
78
|
+
rec.each { |p| ctx.warns << "#{f}: JSON-LD #{type} (#{path}) lacks recommended #{p.tr('|', '/')}" unless has.(p) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The page node (#webpage) against Open Graph and <html lang>: one source of truth for title, URL and image.
|
|
82
|
+
def meta(ctx, f, html, nodes)
|
|
83
|
+
pg = nodes.find { |n| n.is_a?(Hash) && n["@id"].to_s.end_with?("#webpage") } or return
|
|
84
|
+
og = ->(p) { html[/<meta property="og:#{p}" content="([^"]*)"/, 1]&.then { |s| unescape(s) } }
|
|
85
|
+
{ "url" => pg["url"], "title" => pg["name"], "image" => pg.dig("primaryImageOfPage", "url") }.each do |p, v|
|
|
86
|
+
o, v = og.(p)&.split&.join(" "), v&.split&.join(" ") if p == "title"
|
|
87
|
+
o ||= og.(p)
|
|
88
|
+
ctx.errs << "#{f}: JSON-LD #{p == 'title' ? 'name' : p} #{v.inspect} differs from og:#{p} #{o.inspect}" if o && v && o != v
|
|
89
|
+
end
|
|
90
|
+
lang = html[/<html[^>]*\blang="([^"]+)"/, 1]
|
|
91
|
+
ctx.errs << "#{f}: JSON-LD inLanguage #{pg['inLanguage']} differs from <html lang=\"#{lang}\">" if lang && pg["inLanguage"] && lang != pg["inLanguage"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def unescape(s) = s.gsub(""", '"').gsub("'", "'").gsub("<", "<").gsub(">", ">").gsub("&", "&")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# 5b/5c. Site sources: Sass portability on GitHub Pages (Ruby Sass 3.7: @use/@forward pass
|
|
6
|
+
# through silently, @supports selector() is a syntax error).
|
|
7
|
+
module Stylesheets
|
|
8
|
+
SASS_BAD = [[/^\s*@(use|forward)\b/, "@use/@forward (Ruby Sass on GitHub Pages copies it into the CSS untouched); use @import"],
|
|
9
|
+
[/@supports\s+(not\s+)?selector\(/, "@supports selector() (Ruby Sass syntax error on GitHub Pages)"]].freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(ctx)
|
|
14
|
+
sass(ctx)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def sass(ctx)
|
|
18
|
+
src = ctx.src
|
|
19
|
+
Dir.glob(["#{src}/_sass/**/*.scss", "#{src}/assets/**/*.scss"]).reject { |f| ctx.skipped?(f) }.sort.each do |f|
|
|
20
|
+
File.read(f).gsub(%r{/\*.*?\*/}m) { |c| "\n" * c.count("\n") }.each_line.with_index(1) do |l, n|
|
|
21
|
+
l = l.sub(%r{//.*}, "")
|
|
22
|
+
SASS_BAD.each { |re, why| ctx.errs << "#{ctx.rel(f)}:#{n}: #{why}" if l =~ re }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# 7. --twins: every Markdown page (except README.md and `twin: false`) has /<page>/index.md
|
|
6
|
+
# (/index.md for index.md), byte-identical to its source body. It comes from a stub <name>.md.txt
|
|
7
|
+
# (layout: twin; works on GitHub Pages) or from the opt-in plugin (plugins: [jekyll-theme-modootoday]).
|
|
8
|
+
# With modootoday.twins: source, a page without front matter is its own twin at /<path>.md.
|
|
9
|
+
# llms.txt / llms-full.txt exist at / and at each /<lang>/; llms-full.txt contains every page body.
|
|
10
|
+
module Twins
|
|
11
|
+
HINT = "add a stub %s (front matter `layout: twin`, `twin_of`, `permalink`) or enable `plugins: [jekyll-theme-modootoday]` (not on classic GitHub Pages)"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def call(ctx)
|
|
16
|
+
return unless ctx.option?("--twins")
|
|
17
|
+
bodies = Hash.new { |h, k| h[k] = [] }
|
|
18
|
+
ctx.mds.each { |f| page(ctx, f, bodies) }
|
|
19
|
+
llms(ctx, bodies)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def page(ctx, f, bodies)
|
|
23
|
+
errs, site, rf = ctx.errs, ctx.site, ctx.rel(f)
|
|
24
|
+
stub = "#{f}.txt"
|
|
25
|
+
if File.exist?(stub)
|
|
26
|
+
st = File.read(stub)
|
|
27
|
+
meta = (Markdown.fm(st) rescue {})
|
|
28
|
+
errs << "#{ctx.rel(stub)}: stub must set layout: twin" unless meta["layout"] == "twin"
|
|
29
|
+
errs << "#{ctx.rel(stub)}: stub body must be empty" unless Markdown.body(st).strip.empty?
|
|
30
|
+
end
|
|
31
|
+
return if File.basename(f) == "README.md" || (Markdown.fm(File.read(f)) rescue {})["twin"] == false
|
|
32
|
+
return if rf.split("/").include?("_posts") # posts: dated URLs, listed by the archive; no twin (documented limit)
|
|
33
|
+
r = rf.delete_suffix(".md")
|
|
34
|
+
if ctx.source_mode? && File.read(f) !~ Markdown::FM
|
|
35
|
+
# front-matter-less mode: rendered page + raw copy of the whole source at its own path
|
|
36
|
+
errs << "#{rf}.txt: redundant stub in twins: source mode (the source is already the twin); delete it" if File.exist?(stub)
|
|
37
|
+
raw = "#{site}/#{rf}"
|
|
38
|
+
html = ["#{site}/#{r}.html", "#{site}/#{r}/index.html"]
|
|
39
|
+
bodies[ctx.lang_of(r).to_s] << [rf, File.read(f)]
|
|
40
|
+
errs << "#{rf}: no rendered page (#{html.map { |h| h.delete_prefix("#{site}/") }.join(' or ')}); is jekyll-optional-front-matter enabled? (local builds: list it in plugins:)" unless html.any? { |h| File.exist?(h) }
|
|
41
|
+
return errs << "#{rf}: raw copy missing at #{raw} (optional_front_matter.remove_originals must not be true)" unless File.exist?(raw)
|
|
42
|
+
return errs << "#{raw}: raw copy is not byte-identical to #{rf}" unless File.binread(raw) == File.binread(f)
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
out = File.basename(r) == "index" ? "#{site}/#{r}.md" : "#{site}/#{r}/index.md"
|
|
46
|
+
b = Markdown.body(File.read(f))
|
|
47
|
+
bodies[ctx.lang_of(r).to_s] << [rf, b]
|
|
48
|
+
return errs << "#{rf}: twin missing at #{out}: #{format(HINT, "#{rf}.txt")}" unless File.exist?(out)
|
|
49
|
+
errs << "#{out}: twin is not byte-identical to the body of #{rf}" unless File.binread(out) == b.b
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def llms(ctx, bodies)
|
|
53
|
+
langs = [""] + ctx.i18n.keys.select { |l| Dir.exist?("#{ctx.src}/#{l}") }
|
|
54
|
+
langs.each do |l|
|
|
55
|
+
%w[llms.txt llms-full.txt].each do |n|
|
|
56
|
+
p = [ctx.site, l, n].reject(&:empty?).join("/")
|
|
57
|
+
next ctx.errs << "#{p}: missing (per-language llms file; stub with `layout: #{n.delete_suffix('.txt')}` or the plugin)" unless File.exist?(p)
|
|
58
|
+
txt = File.read(p)
|
|
59
|
+
if n == "llms.txt"
|
|
60
|
+
ctx.errs << "#{p}: must start with an H1 (llmstxt.org)" unless txt.start_with?("# ")
|
|
61
|
+
else
|
|
62
|
+
bodies[l].each { |(name, b)| ctx.errs << "#{p}: does not contain the body of #{name}" unless b.strip.empty? || txt.include?(b.strip) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# md-components v1 checker (Ruby stdlib only; Nokogiri optional). This file only assembles the parts;
|
|
4
|
+
# each rule group lives in check/<name>.rb and reports into one shared Context.
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "date"
|
|
7
|
+
require_relative "check/markdown"
|
|
8
|
+
require_relative "check/context"
|
|
9
|
+
require_relative "check/registry"
|
|
10
|
+
require_relative "check/pages"
|
|
11
|
+
require_relative "check/i18n"
|
|
12
|
+
require_relative "check/built_parts"
|
|
13
|
+
require_relative "check/build_output"
|
|
14
|
+
require_relative "check/structured_data"
|
|
15
|
+
require_relative "check/stylesheets"
|
|
16
|
+
require_relative "check/site_config"
|
|
17
|
+
require_relative "check/blog"
|
|
18
|
+
require_relative "check/github_pages"
|
|
19
|
+
require_relative "check/layouts"
|
|
20
|
+
require_relative "check/twins"
|
|
21
|
+
require_relative "check/gh_render"
|
|
22
|
+
require_relative "check/cli"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
# schema.org rules the checker applies to built JSON-LD: the vocabulary (vocab.txt, from script/schema-vocab),
|
|
5
|
+
# required properties per type (schema.org and Google Search structured data guidelines) and recommended ones.
|
|
6
|
+
module Schema
|
|
7
|
+
VOCAB_FILE = File.expand_path("vocab.txt", __dir__)
|
|
8
|
+
# Not schema.org properties, but documented by Google/schema.org Actions for SearchAction.
|
|
9
|
+
EXTRA_PROPS = %w[query-input].freeze
|
|
10
|
+
ARTICLE = %w[Article BlogPosting NewsArticle TechArticle Report ScholarlyArticle SocialMediaPosting].freeze
|
|
11
|
+
PAGE = %w[WebPage AboutPage CollectionPage ContactPage FAQPage ProfilePage SearchResultsPage ItemPage QAPage].freeze
|
|
12
|
+
# type => [required, recommended]; one alternative per entry ("a|b": either).
|
|
13
|
+
RULES = {
|
|
14
|
+
"WebSite" => [%w[name url], %w[inLanguage publisher]],
|
|
15
|
+
"Organization" => [%w[name], %w[url logo]],
|
|
16
|
+
"Person" => [%w[name], %w[url]],
|
|
17
|
+
"BreadcrumbList" => [%w[itemListElement], []],
|
|
18
|
+
"ListItem" => [%w[position], %w[name item|url]],
|
|
19
|
+
"ItemList" => [%w[itemListElement], []],
|
|
20
|
+
"JobPosting" => [%w[title description datePosted hiringOrganization jobLocation|applicantLocationRequirements], %w[employmentType validThrough baseSalary]],
|
|
21
|
+
"Event" => [%w[name startDate location], %w[endDate eventStatus image description organizer offers]],
|
|
22
|
+
"Course" => [%w[name description], %w[provider offers hasCourseInstance]],
|
|
23
|
+
"PodcastEpisode" => [%w[name], %w[partOfSeries datePublished associatedMedia|audio]],
|
|
24
|
+
"SoftwareApplication" => [%w[name], %w[offers applicationCategory operatingSystem]],
|
|
25
|
+
"Offer" => [%w[price|priceSpecification priceCurrency|priceSpecification], %w[availability url]],
|
|
26
|
+
"DefinedTermSet" => [%w[name], %w[hasDefinedTerm]],
|
|
27
|
+
"DefinedTerm" => [%w[name], %w[description]],
|
|
28
|
+
"Question" => [%w[name acceptedAnswer|suggestedAnswer], []],
|
|
29
|
+
"Answer" => [%w[text], []],
|
|
30
|
+
"SearchAction" => [%w[target query-input], []],
|
|
31
|
+
"ImageObject" => [%w[url|contentUrl], []],
|
|
32
|
+
}.merge(ARTICLE.to_h { |t| [t, [%w[headline], %w[datePublished dateModified author image]]] })
|
|
33
|
+
.merge(PAGE.to_h { |t| [t, [%w[name url], %w[inLanguage]]] })
|
|
34
|
+
.merge("FAQPage" => [%w[name url mainEntity], []], "ProfilePage" => [%w[name url], %w[mainEntity]]).freeze
|
|
35
|
+
URL_KEYS = %w[@id url item logo image sameAs contentUrl embedUrl urlTemplate applicationUrl downloadUrl installUrl].freeze
|
|
36
|
+
DATE_KEYS = %w[datePublished dateModified dateCreated datePosted validThrough startDate endDate uploadDate].freeze
|
|
37
|
+
ISO_8601 = /\A\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?\z/
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
def vocab
|
|
42
|
+
@vocab ||= File.readlines(VOCAB_FILE, chomp: true).reject { |l| l.start_with?("#") }
|
|
43
|
+
.each_with_object({ "t" => {}, "p" => {} }) { |l, h| k, n = l.split(" ", 2); h[k][n] = true }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def type?(name) = vocab["t"].key?(name)
|
|
47
|
+
def property?(name) = vocab["p"].key?(name) || EXTRA_PROPS.include?(name)
|
|
48
|
+
end
|
|
49
|
+
end
|