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,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# 6. --github-pages[=VERSION]: the lockfile pins github-pages VERSION (default 232 or $MODOOTODAY_GITHUB_PAGES).
|
|
6
|
+
module GithubPages
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def call(ctx)
|
|
10
|
+
return unless (gp = ctx.argv.find { |a| a == "--github-pages" || a.start_with?("--github-pages=") })
|
|
11
|
+
want = gp.include?("=") ? gp.split("=", 2)[1] : ENV.fetch("MODOOTODAY_GITHUB_PAGES", "232")
|
|
12
|
+
lock = "#{ctx.src}/Gemfile.lock" # the site's own lockfile wins; else the active bundle's lockfile
|
|
13
|
+
lock = "#{ENV['BUNDLE_GEMFILE']}.lock" if !File.exist?(lock) && ENV["BUNDLE_GEMFILE"] && File.exist?("#{ENV['BUNDLE_GEMFILE']}.lock")
|
|
14
|
+
if File.exist?(lock) && (v = File.read(lock)[/^ github-pages \((\d+)\)/, 1])
|
|
15
|
+
ctx.errs << "#{lock}: github-pages #{v} != #{want}" unless v == want
|
|
16
|
+
else
|
|
17
|
+
ctx.errs << "--github-pages: no Gemfile.lock with github-pages found (#{lock}); run `bundle lock` (the file may stay gitignored) " \
|
|
18
|
+
"or point BUNDLE_GEMFILE at a Gemfile whose .lock pins github-pages, or drop --github-pages"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Modootoday
|
|
6
|
+
module Check
|
|
7
|
+
# Interface catalogs: site _data/i18n/<lang>.yml against the default language and the theme's
|
|
8
|
+
# built-in catalogs (i18n/<code>.yml in the gem), --i18n-init and --i18n.
|
|
9
|
+
module I18n
|
|
10
|
+
THEME = File.expand_path("../../../i18n", __dir__)
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def leaves(h, pre = nil) = h.is_a?(Hash) ? h.flat_map { |k, v| leaves(v, [pre, k].compact.join(".")) } : [[pre, h]]
|
|
15
|
+
def keys(h) = leaves(h).map(&:first)
|
|
16
|
+
def theme_codes = Dir.glob("#{THEME}/*.yml").map { |f| File.basename(f, ".yml") } - %w[languages glossary]
|
|
17
|
+
|
|
18
|
+
# Keys the theme ships for a language (exact code, else its base subtag; never ur -> ur-Latn).
|
|
19
|
+
def theme_keys(lang)
|
|
20
|
+
f = "#{THEME}/#{lang}.yml"
|
|
21
|
+
f = Dir.glob("#{THEME}/#{lang.split('-').first}-*.yml").reject { |x| x.end_with?("/ur-Latn.yml") }.min unless File.file?(f)
|
|
22
|
+
f ? keys(YAML.safe_load(File.read(f))) : []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def base_lang(ctx)
|
|
26
|
+
b = ctx.site_cfg["lang"].to_s.split("-").first
|
|
27
|
+
b.to_s.empty? ? "en" : b
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(ctx)
|
|
31
|
+
Dir.glob("#{ctx.src}/_data/i18n/*.yaml").each { |f| ctx.errs << "#{ctx.rel(f)}: catalogs use .yml; rename to #{File.basename(f, '.yaml')}.yml" }
|
|
32
|
+
init(ctx) if ctx.argv.any? { |a| a.start_with?("--i18n-init") }
|
|
33
|
+
follow_default(ctx)
|
|
34
|
+
registry(ctx) if ctx.option?("--i18n") && File.exist?("#{THEME}/en.yml")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def init(ctx)
|
|
38
|
+
ini = ctx.argv.find { |a| a.start_with?("--i18n-init") }
|
|
39
|
+
want = ini.split("=", 2)[1].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
40
|
+
want = theme_codes if want.empty? || want == ["all"]
|
|
41
|
+
FileUtils.mkdir_p("#{ctx.src}/_data/i18n")
|
|
42
|
+
want.each do |l|
|
|
43
|
+
from, to = "#{THEME}/#{l}.yml", "#{ctx.src}/_data/i18n/#{l}.yml"
|
|
44
|
+
next ctx.errs << "--i18n-init: no starter catalog for '#{l}' (have: #{theme_codes.sort.join(' ')})" unless File.exist?(from)
|
|
45
|
+
next (FileUtils.cp(from, to); puts "created #{to}") unless File.exist?(to)
|
|
46
|
+
ref = YAML.safe_load(File.read(from))
|
|
47
|
+
(keys(ref) - keys(YAML.safe_load(File.read(to)) || {})).each { |k| puts "#{to}: missing #{k}: #{k.split('.').reduce(ref) { |a, x| a[x] }.inspect}" }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Translations follow the default language: a translated key must exist in the default file or the
|
|
52
|
+
# theme registry; a key the default file has that a translation lacks (and the theme does not ship
|
|
53
|
+
# for that language) is a warning; a page under <lang>/ needs a default-language page.
|
|
54
|
+
def follow_default(ctx)
|
|
55
|
+
bl = base_lang(ctx)
|
|
56
|
+
return unless (base = ctx.i18n[bl])
|
|
57
|
+
|
|
58
|
+
base_keys = keys(base) | theme_keys("en")
|
|
59
|
+
ctx.i18n.each do |lang, h|
|
|
60
|
+
next if lang == bl
|
|
61
|
+
|
|
62
|
+
(keys(h) - base_keys).each { |k| ctx.errs << "_data/i18n/#{lang}.yml: key '#{k}' is not in _data/i18n/#{bl}.yml (#{bl} is canonical; add it there first)" }
|
|
63
|
+
(keys(base) - keys(h) - theme_keys(lang)).each { |k| ctx.warns << "_data/i18n/#{lang}.yml: missing key '#{k}' (falls back to #{bl})" }
|
|
64
|
+
ctx.mds.each do |f|
|
|
65
|
+
r = ctx.rel(f)
|
|
66
|
+
next unless r.start_with?("#{lang}/")
|
|
67
|
+
|
|
68
|
+
en = r.delete_prefix("#{lang}/")
|
|
69
|
+
ctx.warns << "#{r}: no #{bl} page at #{en} (translations follow the canonical page)" unless File.exist?("#{ctx.src}/#{en}")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def registry(ctx)
|
|
75
|
+
reg = leaves(YAML.safe_load(File.read("#{THEME}/en.yml"))).to_h
|
|
76
|
+
site_en = leaves(ctx.i18n[base_lang(ctx)] || {}).to_h
|
|
77
|
+
ph = ->(v) { v.to_s.scan(/%\{(\w+)\}/).flatten.sort }
|
|
78
|
+
ctx.i18n.each do |lang, h|
|
|
79
|
+
leaves(h).each do |k, v|
|
|
80
|
+
next if %w[title description].include?(k) # site-level strings, not theme keys
|
|
81
|
+
|
|
82
|
+
next ctx.errs << "_data/i18n/#{lang}.yml: unknown key '#{k}' (not in the theme registry or #{base_lang(ctx)}.yml)" unless reg.key?(k) || site_en.key?(k)
|
|
83
|
+
|
|
84
|
+
ref = reg.key?(k) ? reg[k] : site_en[k]
|
|
85
|
+
ctx.errs << "_data/i18n/#{lang}.yml: '#{k}' placeholders #{ph[v].inspect} differ from English #{ph[ref].inspect}" unless ph[v] == ph[ref]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
ctx.mds.each { |f| ctx.warns << "#{ctx.rel(f)}: translation: pending (served as the labeled English fallback)" if (Markdown.fm(File.read(f)) rescue {})["translation"] == "pending" }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
module Layouts
|
|
6
|
+
# Detail layouts (project, case-study, job, event, episode): the front matter each one reads for its facts,
|
|
7
|
+
# call to action and JSON-LD (_includes/modootoday/detail/).
|
|
8
|
+
module Detail
|
|
9
|
+
LAYOUTS = %w[project case-study job event episode].freeze
|
|
10
|
+
EMPLOYMENT = %w[full-time part-time contractor temporary intern volunteer per-diem other].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(errs, where, fm, _ctx)
|
|
15
|
+
links(errs, where, fm)
|
|
16
|
+
send(fm["layout"].tr("-", "_"), errs, where, fm) if respond_to?(fm["layout"].tr("-", "_"))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def links(errs, where, fm)
|
|
20
|
+
Shape.list(errs, where, "links", fm["links"], any: %w[label]) { |l, i| errs << "#{where}: links[#{i}].url is required" unless Shape.url?(l["url"]) }
|
|
21
|
+
errs << "#{where}: stack must be a list" unless fm["stack"].nil? || fm["stack"].is_a?(Array)
|
|
22
|
+
cta = fm["cta"]
|
|
23
|
+
errs << "#{where}: cta must be a map of label and url" unless cta.nil? || (cta.is_a?(Hash) && Shape.text?(cta["label"]) && Shape.url?(cta["url"]))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def job(errs, where, fm)
|
|
27
|
+
Shape.need_date(errs, where, "date", fm["date"])
|
|
28
|
+
Shape.need_date(errs, where, "deadline", fm["deadline"], optional: true)
|
|
29
|
+
Shape.one_of(errs, where, "employment", fm["employment"], EMPLOYMENT)
|
|
30
|
+
errs << "#{where}: set location: (a place) or remote: (the country applicants must live in)" unless Shape.text?(fm["location"]) || Shape.text?(fm["remote"])
|
|
31
|
+
errs << "#{where}: apply must be a site path, full URL or mailto: link" unless fm["apply"].nil? || Shape.url?(fm["apply"])
|
|
32
|
+
s = fm["salary"]
|
|
33
|
+
errs << "#{where}: salary must be text or a map of min, max, currency, unit" unless s.nil? || s.is_a?(String) || (s.is_a?(Hash) && s["min"].is_a?(Numeric) && Shape.text?(s["currency"]))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def event(errs, where, fm)
|
|
37
|
+
Shape.need_date(errs, where, "starts", fm["starts"])
|
|
38
|
+
Shape.need_date(errs, where, "ends", fm["ends"], optional: true)
|
|
39
|
+
errs << "#{where}: set venue: (a place) or online: (an https URL)" unless Shape.text?(fm["venue"]) || fm["online"].to_s.start_with?("https://")
|
|
40
|
+
errs << "#{where}: tickets must be a site path or full URL" unless fm["tickets"].nil? || Shape.url?(fm["tickets"])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def episode(errs, where, fm)
|
|
44
|
+
errs << "#{where}: episode must be a number" unless fm["episode"].is_a?(Integer)
|
|
45
|
+
errs << "#{where}: season must be a number" unless fm["season"].nil? || fm["season"].is_a?(Integer)
|
|
46
|
+
errs << "#{where}: duration must be minutes (a number)" unless fm["duration"].nil? || fm["duration"].is_a?(Integer)
|
|
47
|
+
%w[audio transcript].each { |k| errs << "#{where}: #{k} must be a site path or full URL" unless fm[k].nil? || Shape.url?(fm[k]) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
module Layouts
|
|
6
|
+
# directory and its presets: items: (or data: <file in _data>), preset:, filter: <item field> or false.
|
|
7
|
+
module Directory
|
|
8
|
+
PRESETS = %w[cards portfolio team jobs events sponsors integrations].freeze
|
|
9
|
+
LAYOUTS = %w[directory portfolio team jobs events sponsors integrations].freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(errs, where, fm, ctx)
|
|
14
|
+
Shape.one_of(errs, where, "preset", fm["preset"], PRESETS)
|
|
15
|
+
errs << "#{where}: filter must be an item field name or false" unless fm["filter"].nil? || fm["filter"] == false || fm["filter"].is_a?(String)
|
|
16
|
+
items = items(errs, where, fm, ctx)
|
|
17
|
+
return if items.nil?
|
|
18
|
+
events = fm["layout"] == "events" || fm["preset"] == "events"
|
|
19
|
+
Shape.list(errs, where, "items", items, any: %w[title name]) do |it, i|
|
|
20
|
+
Shape.need_date(errs, where, "items[#{i}].date", it["date"], optional: !events)
|
|
21
|
+
Shape.need_date(errs, where, "items[#{i}].end", it["end"], optional: true)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def items(errs, where, fm, ctx)
|
|
26
|
+
return fm["items"] if fm.key?("items")
|
|
27
|
+
return errs << "#{where}: set items: (a list) or data: (a file in _data)" && nil unless fm["data"]
|
|
28
|
+
f = Dir.glob("#{ctx.src}/_data/#{fm['data']}.{yml,yaml,json}").first
|
|
29
|
+
return errs << "#{where}: data: #{fm['data']} has no file in _data" && nil unless f
|
|
30
|
+
d = YAML.safe_load(File.read(f), permitted_classes: [Date, Time])
|
|
31
|
+
d.is_a?(Hash) ? d["items"] : d
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
module Layouts
|
|
6
|
+
# Docs-extra layouts (reference, changelog, roadmap, glossary, hub, course, book) and the documents legal and resume.
|
|
7
|
+
module DocsExtra
|
|
8
|
+
LAYOUTS = %w[reference changelog roadmap glossary hub course book legal resume].freeze
|
|
9
|
+
CATS = %w[added changed fixed removed security].freeze
|
|
10
|
+
STATUS = %w[now next later].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def url(errs, where, key, v) = (errs << "#{where}: #{key} must be a site path or a full URL" unless v.nil? || Shape.url?(v))
|
|
15
|
+
|
|
16
|
+
def call(errs, w, fm, _ctx)
|
|
17
|
+
send(fm["layout"], errs, w, fm)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reference(errs, w, fm) = Shape.list(errs, w, "members", fm["members"], any: %w[name], optional: false)
|
|
21
|
+
|
|
22
|
+
def changelog(errs, w, fm)
|
|
23
|
+
Shape.list(errs, w, "releases", fm["releases"], any: %w[version], optional: false) do |r, i|
|
|
24
|
+
Shape.need_date(errs, w, "releases[#{i}].date", r["date"], optional: true)
|
|
25
|
+
CATS.each { |c| errs << "#{w}: releases[#{i}].#{c} must be a list" unless r[c].nil? || r[c].is_a?(Array) }
|
|
26
|
+
extra = r.keys - CATS - %w[version date url]
|
|
27
|
+
errs << "#{w}: releases[#{i}] has unknown keys #{extra.join(', ')} (use #{CATS.join(', ')})" unless extra.empty?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def roadmap(errs, w, fm)
|
|
32
|
+
Shape.list(errs, w, "items", fm["items"], optional: false) do |it, i|
|
|
33
|
+
Shape.one_of(errs, w, "items[#{i}].status", it["status"] || "(none)", STATUS)
|
|
34
|
+
url(errs, w, "items[#{i}].url", it["url"])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def glossary(errs, w, fm)
|
|
39
|
+
Shape.list(errs, w, "terms", fm["terms"], any: %w[term], optional: false) do |t, i|
|
|
40
|
+
errs << "#{w}: terms[#{i}].text is required" unless Shape.text?(t["text"])
|
|
41
|
+
end
|
|
42
|
+
names = Array(fm["terms"]).filter_map { |t| t["term"].to_s.downcase if t.is_a?(Hash) }
|
|
43
|
+
names.tally.each { |n, c| errs << "#{w}: term '#{n}' is listed #{c} times" if c > 1 }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def hub(errs, w, fm)
|
|
47
|
+
Shape.list(errs, w, "cards", fm["cards"], optional: false) { |c, i| errs << "#{w}: cards[#{i}].url is required" unless Shape.url?(c["url"]) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def course(errs, w, fm)
|
|
51
|
+
errs << "#{w}: description is required (Course structured data)" unless Shape.text?(fm["description"])
|
|
52
|
+
Shape.list(errs, w, "modules", fm["modules"], optional: false) do |m, i|
|
|
53
|
+
Shape.list(errs, w, "modules[#{i}].lessons", m["lessons"]) do |l, j|
|
|
54
|
+
errs << "#{w}: modules[#{i}].lessons[#{j}].minutes must be a number" unless l["minutes"].nil? || l["minutes"].is_a?(Integer)
|
|
55
|
+
url(errs, w, "modules[#{i}].lessons[#{j}].url", l["url"])
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def book(errs, w, fm)
|
|
61
|
+
errs << "#{w}: pages must be a number" unless fm["pages"].nil? || fm["pages"].is_a?(Integer)
|
|
62
|
+
errs << "#{w}: isbn must be 10 or 13 digits" unless fm["isbn"].nil? || fm["isbn"].to_s.delete("- ").match?(/\A(\d{9}[\dX]|\d{13})\z/)
|
|
63
|
+
Shape.list(errs, w, "chapters", fm["chapters"]) { |c, i| url(errs, w, "chapters[#{i}].url", c["url"]) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def legal(errs, w, fm)
|
|
67
|
+
Shape.need_date(errs, w, "effective", fm["effective"])
|
|
68
|
+
Shape.need_date(errs, w, "updated", fm["updated"], optional: true)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def resume(errs, w, fm)
|
|
72
|
+
errs << "#{w}: set name: (or a title)" unless Shape.text?(fm["name"]) || Shape.text?(fm["title"])
|
|
73
|
+
Shape.list(errs, w, "experience", fm["experience"], any: %w[role]) do |e, i|
|
|
74
|
+
Shape.need_date(errs, w, "experience[#{i}].start", e["start"], optional: true)
|
|
75
|
+
Shape.need_date(errs, w, "experience[#{i}].end", e["end"], optional: true)
|
|
76
|
+
end
|
|
77
|
+
Shape.list(errs, w, "education", fm["education"], any: %w[school])
|
|
78
|
+
Shape.list(errs, w, "skills", fm["skills"], any: %w[group]) { |s, i| errs << "#{w}: skills[#{i}].items must be a list" unless s["items"].is_a?(Array) }
|
|
79
|
+
Shape.list(errs, w, "links", fm["links"], any: %w[label]) { |l, i| url(errs, w, "links[#{i}].url", l["url"]) }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
module Layouts
|
|
6
|
+
# Marketing layouts (feature, pricing, download, brand, links, contact): the lists each one reads from front matter.
|
|
7
|
+
module Marketing
|
|
8
|
+
LAYOUTS = %w[feature pricing download brand links contact].freeze
|
|
9
|
+
HEX = /\A#(\h{3}|\h{6})\z/
|
|
10
|
+
FIELDS = %w[name email message].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(errs, where, fm, _ctx)
|
|
15
|
+
w = where
|
|
16
|
+
link_list(errs, w, "actions", fm["actions"])
|
|
17
|
+
case fm["layout"]
|
|
18
|
+
when "feature" then Shape.list(errs, w, "features", fm["features"])
|
|
19
|
+
when "pricing" then pricing(errs, w, fm)
|
|
20
|
+
when "download"
|
|
21
|
+
Shape.list(errs, w, "downloads", fm["downloads"], any: %w[label], optional: false) { |d, i| need_url(errs, w, "downloads[#{i}].url", d["url"]) }
|
|
22
|
+
when "brand" then brand(errs, w, fm)
|
|
23
|
+
when "links" then link_list(errs, w, "links", fm["links"], optional: false)
|
|
24
|
+
when "contact" then contact(errs, w, fm)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def need_url(errs, where, key, v) = (errs << "#{where}: #{key} must be a site path or a full URL" unless Shape.url?(v))
|
|
29
|
+
|
|
30
|
+
def link_list(errs, where, key, val, optional: true)
|
|
31
|
+
Shape.list(errs, where, key, val, any: %w[label], optional: optional) { |l, i| need_url(errs, where, "#{key}[#{i}].url", l["url"]) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def pricing(errs, where, fm)
|
|
35
|
+
plans = fm["plans"]
|
|
36
|
+
Shape.list(errs, where, "plans", plans, any: %w[name], optional: false) do |p, i|
|
|
37
|
+
errs << "#{where}: plans[#{i}].monthly is required" if p["monthly"].nil?
|
|
38
|
+
errs << "#{where}: plans[#{i}].features must be a list" unless p["features"].nil? || p["features"].is_a?(Array)
|
|
39
|
+
end
|
|
40
|
+
return unless plans.is_a?(Array)
|
|
41
|
+
y = plans.count { |p| p.is_a?(Hash) && !p["yearly"].nil? }
|
|
42
|
+
errs << "#{where}: set yearly on every plan or on none (#{y} of #{plans.size})" unless [0, plans.size].include?(y)
|
|
43
|
+
errs << "#{where}: only one plan can be recommended" if plans.count { |p| p.is_a?(Hash) && p["recommended"] } > 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def brand(errs, where, fm)
|
|
47
|
+
Shape.list(errs, where, "colors", fm["colors"], any: %w[name]) do |c, i|
|
|
48
|
+
errs << "#{where}: colors[#{i}].hex must be #rgb or #rrggbb" unless c["hex"].is_a?(String) && c["hex"].match?(HEX)
|
|
49
|
+
end
|
|
50
|
+
Shape.list(errs, where, "assets", fm["assets"], any: %w[label]) { |a, i| need_url(errs, where, "assets[#{i}].url", a["url"]) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def contact(errs, where, fm)
|
|
54
|
+
Shape.list(errs, where, "contacts", fm["contacts"], any: %w[label])
|
|
55
|
+
f = fm["form"]
|
|
56
|
+
return if f.nil?
|
|
57
|
+
return errs << "#{where}: form must be a map with action (an https URL)" unless f.is_a?(Hash) && f["action"].to_s.start_with?("https://")
|
|
58
|
+
fl = f["fields"].is_a?(String) ? f["fields"].split(",") : f["fields"]
|
|
59
|
+
(Array(fl) - FIELDS).each { |x| errs << "#{where}: form.fields has unknown field '#{x}' (known: #{FIELDS.join(', ')})" }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
module Layouts
|
|
6
|
+
# Shape helpers shared by the layout contracts: lists of maps, required keys, dates and URLs.
|
|
7
|
+
module Shape
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def text?(v) = v.is_a?(String) && !v.strip.empty?
|
|
11
|
+
def url?(v) = v.is_a?(String) && v.match?(%r{\A(/|https?://|mailto:|tel:|#)})
|
|
12
|
+
def date?(v) = v.is_a?(Date) || v.is_a?(Time) || (v.is_a?(String) && (Time.parse(v) rescue false))
|
|
13
|
+
|
|
14
|
+
# A list of maps; each map needs one of `any` keys (e.g. title or name). Yields (item, index) for more checks.
|
|
15
|
+
def list(errs, where, key, val, any: %w[title], optional: true)
|
|
16
|
+
return errs << "#{where}: #{key} is required" if val.nil? && !optional
|
|
17
|
+
return if val.nil?
|
|
18
|
+
return errs << "#{where}: #{key} must be a list" unless val.is_a?(Array)
|
|
19
|
+
val.each_with_index do |it, i|
|
|
20
|
+
next errs << "#{where}: #{key}[#{i}] must be a map" unless it.is_a?(Hash)
|
|
21
|
+
errs << "#{where}: #{key}[#{i}] needs #{any.join(' or ')}" unless any.empty? || any.any? { |k| text?(it[k]) || it[k].is_a?(Numeric) }
|
|
22
|
+
%w[url image].each { |k| errs << "#{where}: #{key}[#{i}].#{k} must be a site path or a full URL" if it.key?(k) && !url?(it[k]) }
|
|
23
|
+
yield it, i if block_given?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def one_of(errs, where, key, val, allowed)
|
|
28
|
+
errs << "#{where}: #{key} must be one of #{allowed.join(', ')} (got #{val.inspect})" unless val.nil? || allowed.include?(val)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def need_date(errs, where, key, val, optional: false)
|
|
32
|
+
return if val.nil? && optional
|
|
33
|
+
errs << "#{where}: #{key} must be a date (YYYY-MM-DD or YYYY-MM-DD HH:MM)" unless date?(val)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
module Layouts
|
|
6
|
+
# Page shells: error status, redirect target, home hero; and the search fallback in _config.yml.
|
|
7
|
+
module Shells
|
|
8
|
+
STATUSES = [404, 410, 500].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def error(errs, where, fm, _ctx)
|
|
13
|
+
s = fm.fetch("status", 404)
|
|
14
|
+
errs << "#{where}: status must be one of #{STATUSES.join(', ')} (got #{s.inspect})" unless STATUSES.include?(s)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def redirect(errs, where, fm, _ctx)
|
|
18
|
+
to = fm["redirect_to"]
|
|
19
|
+
errs << "#{where}: redirect_to is required (a site path starting with / or a full URL)" unless to.is_a?(String) && to.match?(%r{\A(/|https?://)})
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def home(errs, where, fm, _ctx)
|
|
23
|
+
hero = fm["hero"]
|
|
24
|
+
return if hero.nil?
|
|
25
|
+
return errs << "#{where}: hero must be a map of title, text, actions, image" unless hero.is_a?(Hash)
|
|
26
|
+
(hero.keys - %w[title text actions image]).each { |k| errs << "#{where}: hero has unknown key '#{k}' (known: title, text, actions, image)" }
|
|
27
|
+
errs << "#{where}: hero.title is required when hero is set" unless Shape.text?(hero["title"])
|
|
28
|
+
actions(errs, where, "hero.actions", hero["actions"])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def actions(errs, where, key, acts)
|
|
32
|
+
return if acts.nil?
|
|
33
|
+
return errs << "#{where}: #{key} must be a list of {label, url}" unless acts.is_a?(Array)
|
|
34
|
+
acts.each_with_index do |a, i|
|
|
35
|
+
errs << "#{where}: #{key}[#{i}] needs label and url" unless a.is_a?(Hash) && a["label"].is_a?(String) && a["url"].is_a?(String)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fallback(errs, fb)
|
|
40
|
+
return if fb.nil? || fb == false
|
|
41
|
+
return errs << "_config.yml: modootoday.search.fallback must be false or a map of url, param" unless fb.is_a?(Hash)
|
|
42
|
+
errs << "_config.yml: modootoday.search.fallback.url must be an https URL" if fb.key?("url") && !fb["url"].to_s.start_with?("https://")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "layouts/shape"
|
|
5
|
+
require_relative "layouts/shells"
|
|
6
|
+
require_relative "layouts/directory"
|
|
7
|
+
require_relative "layouts/detail"
|
|
8
|
+
require_relative "layouts/marketing"
|
|
9
|
+
require_relative "layouts/docs_extra"
|
|
10
|
+
|
|
11
|
+
module Modootoday
|
|
12
|
+
module Check
|
|
13
|
+
# Front matter contracts of the theme layouts: each layout name maps to the check of the keys it reads
|
|
14
|
+
# (lib/modootoday/check/layouts/). Pages with other layouts are left alone.
|
|
15
|
+
module Layouts
|
|
16
|
+
def self.contracts
|
|
17
|
+
@contracts ||= {
|
|
18
|
+
"error" => Shells.method(:error), "redirect" => Shells.method(:redirect), "home" => Shells.method(:home),
|
|
19
|
+
}.merge(Directory::LAYOUTS.to_h { |l| [l, Directory.method(:call)] })
|
|
20
|
+
.merge(Detail::LAYOUTS.to_h { |l| [l, Detail.method(:call)] })
|
|
21
|
+
.merge(Marketing::LAYOUTS.to_h { |l| [l, Marketing.method(:call)] })
|
|
22
|
+
.merge(DocsExtra::LAYOUTS.to_h { |l| [l, DocsExtra.method(:call)] })
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.register(names, check) = names.each { |n| contracts[n] = check }
|
|
26
|
+
|
|
27
|
+
def self.call(ctx)
|
|
28
|
+
ctx.mds.each do |f|
|
|
29
|
+
fm = Markdown.fm(File.read(f))
|
|
30
|
+
check = contracts[fm["layout"]]
|
|
31
|
+
check&.call(ctx.errs, "#{ctx.rel(f)}: layout #{fm['layout']}", fm, ctx)
|
|
32
|
+
end
|
|
33
|
+
Shells.fallback(ctx.errs, ctx.mtc.is_a?(Hash) && ctx.mtc["search"].is_a?(Hash) ? ctx.mtc["search"]["fallback"] : nil)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# Markdown text helpers shared by the rules: front matter, fenced code, code spans, comments.
|
|
6
|
+
module Markdown
|
|
7
|
+
FM = /\A---\r?\n(.*?\r?\n)?---\r?\n/m
|
|
8
|
+
FENCE_OPEN = /^( {0,3})(`{3,}|~{3,})(.*)$/
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def fm(t) = (m = t[FM]) ? (YAML.safe_load(m.sub(/\A---\r?\n/, "").sub(/---\r?\n\z/, ""), permitted_classes: [Date, Time]) || {}) : {}
|
|
13
|
+
def body(t) = t.sub(FM, "")
|
|
14
|
+
|
|
15
|
+
# Blank out fenced code (CommonMark: closing fence = same char, length >= opener), keep line count.
|
|
16
|
+
# Yields [line_no, info_string] for every opening fence.
|
|
17
|
+
def strip_fences(t)
|
|
18
|
+
out = []
|
|
19
|
+
close = nil
|
|
20
|
+
t.each_line.with_index(1) do |l, n|
|
|
21
|
+
if close
|
|
22
|
+
close = nil if l =~ /^ {0,3}#{Regexp.escape(close[0])}{#{close.size},}\s*$/
|
|
23
|
+
out << "\n"
|
|
24
|
+
elsif (m = l.match(FENCE_OPEN)) && !(m[2].start_with?("`") && m[3].include?("`"))
|
|
25
|
+
close = m[2]
|
|
26
|
+
yield n, m[3].strip if block_given?
|
|
27
|
+
out << "\n"
|
|
28
|
+
else
|
|
29
|
+
out << l
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
out.join
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def strip_code(t) = strip_fences(t).gsub(/(`+)(?!`).+?(?<!`)\1(?!`)/) { "``" }
|
|
36
|
+
def strip_comments(t) = t.gsub(/<!--.*?-->/m) { |m| "\n" * m.count("\n") }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# Rules 3-4: the sections: list matches the '## ' headings, and each section's component
|
|
6
|
+
# (from _data/components.yml) gets known, allowed and required params and its slot contract.
|
|
7
|
+
module PageSections
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
THEME = File.expand_path("../../..", __dir__)
|
|
11
|
+
|
|
12
|
+
def call(ctx, f, text, b, code, h2)
|
|
13
|
+
errs, r = ctx.errs, ctx.rel(f)
|
|
14
|
+
meta = begin Markdown.fm(text) rescue (errs << "#{r}: front matter YAML error"; {}) end
|
|
15
|
+
secs = meta["sections"]
|
|
16
|
+
return unless secs
|
|
17
|
+
return errs << "#{r}: sections: must be a list" unless secs.is_a?(Array)
|
|
18
|
+
code.scan(/<!--.*?-->/m) { |c| errs << "#{r}: '##' inside an HTML comment is not a heading" if c =~ /^##/ }
|
|
19
|
+
errs << "#{r}: sections: has #{secs.size} item(s) but body has #{h2} top-level '## ' heading(s)" if secs.size != h2
|
|
20
|
+
sec_md = section_markdown(b, code)
|
|
21
|
+
secs.each_with_index { |s, i| ComponentRules.section(ctx, r, i, s, sec_md[i]) if s.is_a?(Hash) }
|
|
22
|
+
components(ctx, r, secs, sec_md) if ctx.comps
|
|
23
|
+
hero = meta["hero"]
|
|
24
|
+
errs << "#{r}: hero: must be a map such as {visual: name}" if hero && !hero.is_a?(Hash)
|
|
25
|
+
visuals(ctx, r, secs, hero.is_a?(Hash) ? hero : nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# visual: <name> on a section item or on hero: needs _includes/sections/<name>.html in the site or the theme (showcase).
|
|
29
|
+
def visuals(ctx, r, secs, hero)
|
|
30
|
+
items = secs.each_with_index.map { |s, i| [s, "sections[#{i}]"] }
|
|
31
|
+
items << [hero, "hero"] if hero
|
|
32
|
+
items.each do |s, where|
|
|
33
|
+
next unless s.is_a?(Hash) && s.key?("visual")
|
|
34
|
+
v = s["visual"].to_s
|
|
35
|
+
next ctx.errs << "#{r}: #{where}.visual must be an include name [a-z0-9_-] (got #{s['visual'].inspect})" unless v =~ /\A[a-z0-9][a-z0-9_-]*\z/
|
|
36
|
+
ctx.errs << "#{r}: #{where}.visual: _includes/sections/#{v}.html not found" unless [ctx.src, THEME].any? { |d| File.exist?(File.join(d, "_includes/sections/#{v}.html")) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Markdown of each top-level ## section (index i = sections[i]): [code spans/fences blanked, raw]
|
|
41
|
+
def section_markdown(b, code)
|
|
42
|
+
bl = b.lines
|
|
43
|
+
cl = code.lines
|
|
44
|
+
starts = cl.each_index.select { |k| cl[k] =~ /^##(?: |$)/ }
|
|
45
|
+
starts.each_with_index.map do |st, k|
|
|
46
|
+
raw = bl[st + 1...(starts[k + 1] || bl.size)].to_a.join
|
|
47
|
+
[cl[st + 1...(starts[k + 1] || cl.size)].to_a.join, raw]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def components(ctx, r, secs, sec_md)
|
|
52
|
+
errs = ctx.errs
|
|
53
|
+
secs.each_with_index do |s, i|
|
|
54
|
+
next unless s.is_a?(Hash) && s["c"]
|
|
55
|
+
spec = ctx.comps[s["c"]]
|
|
56
|
+
next errs << "#{r}: sections[#{i}] unknown component '#{s['c']}' (not in _data/components.yml)" unless spec
|
|
57
|
+
params = spec["params"] || {}
|
|
58
|
+
Slots.check_section(errs, r, i, s["c"], spec["slots"], sec_md[i])
|
|
59
|
+
(ctx.part_uses[s["c"]] ||= []) << r if spec.is_a?(Hash) && spec["parts"].is_a?(Array)
|
|
60
|
+
(s.keys - %w[c name visual]).each do |k|
|
|
61
|
+
next errs << "#{r}: sections[#{i}] #{s['c']}: unknown param '#{k}'" unless params.key?(k)
|
|
62
|
+
allowed = params[k].is_a?(Hash) && params[k]["allowed"]
|
|
63
|
+
errs << "#{r}: sections[#{i}] #{s['c']}.#{k}=#{s[k].inspect} not in #{allowed.inspect}" if allowed && !allowed.include?(s[k])
|
|
64
|
+
end
|
|
65
|
+
params.each do |k, p|
|
|
66
|
+
errs << "#{r}: sections[#{i}] #{s['c']}: missing required param '#{k}'" if p.is_a?(Hash) && p["required"] && !s.key?(k)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|