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
data/i18n/zh-TW.yml
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# jekyll-theme-modootoday interface strings (Traditional Chinese). Copy to _data/i18n/zh-TW.yml; keys are fixed, values are yours.
|
|
2
|
+
language: 繁體中文
|
|
3
|
+
direction: ltr
|
|
4
|
+
skip: 跳至主要內容
|
|
5
|
+
new_window: {hint: "在新視窗中開啟", open: "在新視窗中開啟"}
|
|
6
|
+
callout: {note: 注意, tip: 提示, important: 重要, warning: 警告, caution: 小心}
|
|
7
|
+
status: {available: 可用, partial: 部分支援, planned: 規劃中}
|
|
8
|
+
task: {done: 已完成, open: 未完成}
|
|
9
|
+
tabs: {label: 選項}
|
|
10
|
+
toc: {label: 本頁內容}
|
|
11
|
+
theme: {toggle: 深色模式}
|
|
12
|
+
sidebar: {label: "文件"}
|
|
13
|
+
page_actions: {label: "頁面操作", copy: "複製頁面", copy_ai: "複製給 AI", view: "以 Markdown 檢視", source: "來源"}
|
|
14
|
+
breadcrumb: {label: 導覽路徑}
|
|
15
|
+
nav: {label: 主選單, back_to_top: 回到頂端}
|
|
16
|
+
pagination: {label: 換頁, prev: 上一頁, next: 下一頁}
|
|
17
|
+
code: {copy: 複製, copied: 已複製, label: 程式碼, file: 檔案}
|
|
18
|
+
table: {label: 表格}
|
|
19
|
+
search: {label: 搜尋}
|
|
20
|
+
page: {edit: 編輯此頁, updated: 最後更新}
|
|
21
|
+
languages: {label: 語言}
|
|
22
|
+
post: {date_format: "%Y年%-m月%-d日", author: 關於作者, related: 相關文章, share: 分享}
|
|
23
|
+
fallback: {notice: 本頁尚未翻譯,以下顯示英文原文。}
|
|
24
|
+
archive:
|
|
25
|
+
years: "年份"
|
|
26
|
+
tags: "標籤"
|
|
27
|
+
categories: "分類"
|
|
28
|
+
posts: "篇文章"
|
|
29
|
+
empty: "目前還沒有文章。"
|
|
30
|
+
blog:
|
|
31
|
+
featured: "精選"
|
|
32
|
+
read_more: "閱讀全文"
|
|
33
|
+
reading_time: "閱讀約 [n] 分鐘"
|
|
34
|
+
by: "作者"
|
|
35
|
+
updated: "更新於"
|
|
36
|
+
newer: "較新文章"
|
|
37
|
+
older: "較舊文章"
|
|
38
|
+
related: "相關文章"
|
|
39
|
+
series: "系列"
|
|
40
|
+
series_part: "第 [n] 篇,共 [total] 篇"
|
|
41
|
+
share: "複製連結"
|
|
42
|
+
copied: "已複製連結"
|
|
43
|
+
feed: "訂閱源"
|
|
44
|
+
latest: "最新"
|
|
45
|
+
breaking: "快訊"
|
|
46
|
+
live: "直播"
|
|
47
|
+
correction: "更正"
|
|
48
|
+
sources: "來源"
|
|
49
|
+
more: "更多[section]"
|
|
50
|
+
authors: "作者"
|
|
51
|
+
filetree: {label: 檔案結構}
|
|
52
|
+
gallery: {close: 關閉}
|
|
53
|
+
embed: {title: 嵌入內容}
|
|
54
|
+
pricing: {recommended: 推薦}
|
|
55
|
+
banner: {label: 公告}
|
|
56
|
+
footnote: {back: 返回正文}
|
|
57
|
+
progress: {label: 進度}
|
|
58
|
+
subscribe: {email: 電子郵件地址, submit: 訂閱}
|
|
59
|
+
params: {required: 必填}
|
|
60
|
+
loading: {label: 載入中…}
|
|
61
|
+
pages:
|
|
62
|
+
not_found: "找不到頁面"
|
|
63
|
+
not_found_text: "您要求的頁面不存在或已移動。"
|
|
64
|
+
gone: "頁面已移除"
|
|
65
|
+
gone_text: "此頁面已被刻意移除,不會再恢復。"
|
|
66
|
+
error: "發生問題"
|
|
67
|
+
error_text: "伺服器無法顯示此頁面。請稍後再試。"
|
|
68
|
+
home: "返回首頁"
|
|
69
|
+
moved: "此頁面已移動。"
|
|
70
|
+
moved_link: "前往新頁面"
|
|
71
|
+
search_hint: "輸入至少兩個字元開始搜尋。"
|
|
72
|
+
layouts:
|
|
73
|
+
all: "全部"
|
|
74
|
+
filter: "篩選"
|
|
75
|
+
role: "角色"
|
|
76
|
+
client: "客戶"
|
|
77
|
+
year: "年份"
|
|
78
|
+
stack: "工具"
|
|
79
|
+
links: "連結"
|
|
80
|
+
location: "地點"
|
|
81
|
+
employment: "僱用類型"
|
|
82
|
+
salary: "薪資"
|
|
83
|
+
posted: "發布日期"
|
|
84
|
+
deadline: "截止日期"
|
|
85
|
+
apply: "應徵"
|
|
86
|
+
starts: "開始"
|
|
87
|
+
ends: "結束"
|
|
88
|
+
venue: "地點"
|
|
89
|
+
online: "線上"
|
|
90
|
+
agenda: "議程"
|
|
91
|
+
tickets: "取得門票"
|
|
92
|
+
calendar: "加入行事曆"
|
|
93
|
+
episode: "第 [n] 集"
|
|
94
|
+
season: "第 [n] 季"
|
|
95
|
+
duration: "長度"
|
|
96
|
+
listen: "收聽"
|
|
97
|
+
transcript: "逐字稿"
|
|
98
|
+
monthly: "月繳"
|
|
99
|
+
yearly: "年繳"
|
|
100
|
+
billing: "計費週期"
|
|
101
|
+
popular: "最受歡迎"
|
|
102
|
+
download: "下載"
|
|
103
|
+
version: "版本"
|
|
104
|
+
colors: "顏色"
|
|
105
|
+
assets: "素材"
|
|
106
|
+
send: "送出"
|
|
107
|
+
name: "姓名"
|
|
108
|
+
email: "電子郵件"
|
|
109
|
+
message: "訊息"
|
|
110
|
+
contact: "聯絡方式"
|
|
111
|
+
added: "新增"
|
|
112
|
+
changed: "變更"
|
|
113
|
+
fixed: "修正"
|
|
114
|
+
removed: "移除"
|
|
115
|
+
security: "安全性"
|
|
116
|
+
now: "現在"
|
|
117
|
+
next: "下一步"
|
|
118
|
+
later: "之後"
|
|
119
|
+
letters: "跳到字母"
|
|
120
|
+
lessons: "課程"
|
|
121
|
+
minutes: "[n] 分鐘"
|
|
122
|
+
module: "單元 [n]"
|
|
123
|
+
chapter: "第 [n] 章"
|
|
124
|
+
effective: "生效日期"
|
|
125
|
+
experience: "工作經歷"
|
|
126
|
+
education: "學歷"
|
|
127
|
+
skills: "技能"
|
|
128
|
+
present: "至今"
|
|
129
|
+
loading: "載入中…"
|
|
130
|
+
contents: "目錄"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# md-components v1 — optional generator for Jekyll builds that can load plugins
|
|
4
|
+
# (Jekyll 4 / GitHub Actions; NOT the classic GitHub Pages build, which only runs whitelisted plugins).
|
|
5
|
+
# Enable with `plugins: [jekyll-theme-modootoday]`. For every Markdown page without a stub it adds the
|
|
6
|
+
# same virtual page a hand-written stub would be:
|
|
7
|
+
# <name>.md.txt {layout: twin, twin_of: <name>.md, permalink: /<name>/index.md}
|
|
8
|
+
# llms.txt / llms-full.txt at the root and in each /<lang>/ directory (layouts llms / llms-full)
|
|
9
|
+
# so output is identical to the stub-based (GitHub Pages) setup. Existing pages/stubs always win.
|
|
10
|
+
require "jekyll"
|
|
11
|
+
|
|
12
|
+
module JekyllThemeModootoday
|
|
13
|
+
class Stub < Jekyll::PageWithoutAFile
|
|
14
|
+
def initialize(site, dir, name, data)
|
|
15
|
+
super(site, site.source, dir, name)
|
|
16
|
+
self.content = ""
|
|
17
|
+
self.data.merge!(data)
|
|
18
|
+
self.data["sitemap"] = false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Generator < Jekyll::Generator
|
|
23
|
+
safe true
|
|
24
|
+
priority :lowest
|
|
25
|
+
|
|
26
|
+
def generate(site)
|
|
27
|
+
cfg = site.config["modootoday"] || {}
|
|
28
|
+
urls = site.pages.map(&:url).to_h { |u| [u, true] }
|
|
29
|
+
add = lambda do |dir, name, data|
|
|
30
|
+
page = Stub.new(site, dir, name, data)
|
|
31
|
+
next if urls[page.url]
|
|
32
|
+
|
|
33
|
+
urls[page.url] = true
|
|
34
|
+
site.pages << page
|
|
35
|
+
end
|
|
36
|
+
md = site.pages.select { |p| p.is_a?(Jekyll::Page) && !p.is_a?(Stub) && p.ext == ".md" && p.name != "README.md" }
|
|
37
|
+
raw = site.static_files.to_h { |f| [f.relative_path.sub(%r{\A/}, ""), true] }
|
|
38
|
+
if cfg.fetch("twins", true)
|
|
39
|
+
md.each do |p|
|
|
40
|
+
next if p.data["twin"] == false
|
|
41
|
+
next if raw[p.relative_path.sub(%r{\A/}, "")] # front-matter-less mode: the source itself is the twin
|
|
42
|
+
|
|
43
|
+
url = p.url.end_with?("/") ? "#{p.url}index.md" : p.url.sub(/\.html\z/, ".md")
|
|
44
|
+
d = File.dirname(p.path)
|
|
45
|
+
add.call(d == "." ? "" : d, "#{p.name}.txt", "layout" => "twin", "twin_of" => p.name, "permalink" => url)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
return unless cfg.fetch("llms", true)
|
|
49
|
+
|
|
50
|
+
langs = (site.data["i18n"] || {}).keys.select { |l| Dir.exist?(File.join(site.source, l)) }
|
|
51
|
+
([""] + langs).each do |l|
|
|
52
|
+
url = l.empty? ? "/" : "/#{l}/"
|
|
53
|
+
extra = l.empty? ? {} : { "lang" => l }
|
|
54
|
+
add.call(l, "llms.txt", { "layout" => "llms", "permalink" => "#{url}llms.txt" }.merge(extra))
|
|
55
|
+
add.call(l, "llms-full.txt", { "layout" => "llms-full", "permalink" => "#{url}llms-full.txt" }.merge(extra))
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# Authoring-side screen-reader rules (WCAG 1.1.1, 1.3.1, 2.4.4, 4.1.1-style ids) on one page's Markdown,
|
|
6
|
+
# code already blanked. Errors: images with no alt, empty links, duplicate ids. Warnings: heading skips,
|
|
7
|
+
# vague link text, empty Markdown alt (decorative is allowed but should be deliberate).
|
|
8
|
+
module A11yContent
|
|
9
|
+
# English list mirrors Lighthouse SEO "link-text" (a hit there costs the SEO score).
|
|
10
|
+
VAGUE = /\A(click here|click this|here|right here|more|read more|learn more|see more|more info|more information|information|go|start|this|continue|link|여기|여기를 클릭|더 보기|こちら|ここ|詳しくはこちら)\z/i
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(ctx, f, code)
|
|
15
|
+
r = ctx.rel(f)
|
|
16
|
+
images(ctx, r, code)
|
|
17
|
+
links(ctx, r, code)
|
|
18
|
+
headings(ctx, r, code)
|
|
19
|
+
ids(ctx, r, code)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def each_line(code, re)
|
|
23
|
+
code.each_line.with_index(1) { |l, n| l.scan(re) { yield n, Regexp.last_match } }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def images(ctx, r, code)
|
|
27
|
+
each_line(code, /<img\b(?![^>]*\balt=)[^>]*>/i) { |n, _| ctx.errs << "#{r}:#{n}: <img> without alt (screen readers read the file name); add alt=\"…\" or alt=\"\" if decorative" }
|
|
28
|
+
each_line(code, /!\[\s*\]\(/) { |n, _| ctx.warns << "#{r}:#{n}: image with empty alt  is treated as decorative; describe it unless it adds no information" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def links(ctx, r, code)
|
|
32
|
+
each_line(code, /(?<!!)\[([^\]]*)\]\([^)]*\)/) do |n, m|
|
|
33
|
+
t = m[1].gsub(/[*_`]/, "").strip
|
|
34
|
+
next ctx.errs << "#{r}:#{n}: empty link text [](…) (announced as just \"link\"); write what it opens" if t.empty?
|
|
35
|
+
ctx.warns << "#{r}:#{n}: vague link text \"#{t}\"; name the destination so it makes sense out of context" if t.match?(VAGUE)
|
|
36
|
+
end
|
|
37
|
+
each_line(code, %r{<a\b[^>]*>\s*</a>}i) { |n, _| ctx.errs << "#{r}:#{n}: empty <a></a> link" }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The layout supplies the page <h1> (title); content starts at ## and must not skip levels.
|
|
41
|
+
def headings(ctx, r, code)
|
|
42
|
+
prev = 1
|
|
43
|
+
code.each_line.with_index(1) do |l, n|
|
|
44
|
+
next unless (m = l.match(/^ {0,3}(#{"#"}{1,6})\s+\S/))
|
|
45
|
+
lv = m[1].size
|
|
46
|
+
ctx.warns << "#{r}:#{n}: heading jumps from h#{prev} to h#{lv}; screen-reader users navigate by level, do not skip" if lv > prev + 1
|
|
47
|
+
prev = lv
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ids(ctx, r, code)
|
|
52
|
+
seen = Hash.new(0)
|
|
53
|
+
each_line(code, /\bid=["']([^"']+)["']/) { |n, m| ctx.errs << "#{r}:#{n}: duplicate id=\"#{m[1]}\" (breaks labels, skip links and anchors)" if (seen[m[1]] += 1) == 2 }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module Modootoday
|
|
7
|
+
module Check
|
|
8
|
+
# Links that open a new window, and sample links in demo pages.
|
|
9
|
+
# Source: raw <a target="_blank"> without rel="noopener" (error) or without the theme hint (warning; use
|
|
10
|
+
# {: .mt-new-window} or new_window: true); a non-boolean new_window: (error); in a demo page (front matter
|
|
11
|
+
# noindex: true) a link or url: that leaves the sample (root path, other site) or is empty (warning).
|
|
12
|
+
# Built site: target="_blank" without rel="noopener" (error) or without aria-describedby (warning); a demo page
|
|
13
|
+
# link that resolves to the site root (warning: write "#").
|
|
14
|
+
module NewWindow
|
|
15
|
+
BLANK = /<a\b[^>]*\btarget=["']_blank["'][^>]*>/i
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def call(ctx, f, text, code)
|
|
20
|
+
r = ctx.rel(f)
|
|
21
|
+
code.each_line.with_index(1) do |l, n|
|
|
22
|
+
l.scan(BLANK) do |a|
|
|
23
|
+
next if a.include?("mt-new-window") || a.include?("data-mt-new-window")
|
|
24
|
+
next ctx.errs << "#{r}:#{n}: target=\"_blank\" without rel=\"noopener\"; add it or use [text](url \"new-window\")" unless a =~ /\brel=["'][^"']*noopener/
|
|
25
|
+
ctx.warns << "#{r}:#{n}: new-window link without the spoken hint; use [text](url \"new-window\") or new_window: true"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
fm = front(text)
|
|
29
|
+
text[/\A---\s*\n.*?\n---/m].to_s.scan(/\bnew_window:\s*([^,}\s]+)/) { ctx.errs << "#{r}: new_window: must be true or false (got #{$1})" unless %w[true false].include?($1) }
|
|
30
|
+
demo(ctx, r, text, code) if fm["noindex"] == true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def front(text)
|
|
34
|
+
m = text.match(/\A---\s*\n(.*?)\n---/m)
|
|
35
|
+
m ? (YAML.safe_load(m[1], permitted_classes: [Date, Time]) || {}) : {}
|
|
36
|
+
rescue Psych::Exception
|
|
37
|
+
{}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def demo(ctx, r, text, code)
|
|
41
|
+
urls = code.scan(/(?<!!)\[[^\]]*\]\(([^)]*)\)/).flatten + text.scan(/"url":\s*"([^"]*)"|\burl:\s*"?([^",}\s]*)/).flatten.compact
|
|
42
|
+
urls.each do |u|
|
|
43
|
+
next ctx.warns << "#{r}: demo link with no target; write \"#\" for a sample link" if u.strip.empty?
|
|
44
|
+
next unless u.start_with?("/") || u =~ %r{\A\w+://}
|
|
45
|
+
ctx.warns << "#{r}: demo link #{u} leaves the sample; write \"#\" unless it is the real destination"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def built(ctx)
|
|
50
|
+
Dir.glob("#{ctx.site}/**/*.html").each do |f|
|
|
51
|
+
t = File.read(f).gsub(%r{<pre\b.*?</pre>}m, "")
|
|
52
|
+
t.scan(BLANK) do |a|
|
|
53
|
+
next ctx.errs << "#{f}: target=\"_blank\" without rel=\"noopener\"" unless a =~ /\brel=["'][^"']*noopener/
|
|
54
|
+
ctx.warns << "#{f}: new-window link without aria-describedby hint" unless a.include?("aria-describedby")
|
|
55
|
+
end
|
|
56
|
+
next unless t.include?('name="robots" content="noindex"') && !t.include?('http-equiv="refresh"')
|
|
57
|
+
t.scan(/<main\b.*<\/main>/m).join.gsub(%r{<nav[^>]*breadcrumb.*?</nav>}m, "").scan(/<a\b[^>]*\bhref="([^"]*)"/) do |(h)|
|
|
58
|
+
ctx.warns << "#{f}: demo link #{h} resolves to the site root; write \"#\" for a sample link" if h =~ %r{\A(/[\w.-]+)?/#?\z}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# Blog and news: modootoday.blog / modootoday.news settings, _data/authors.yml and post or listing front matter.
|
|
6
|
+
module Blog
|
|
7
|
+
VARIANTS = %w[list grid magazine].freeze
|
|
8
|
+
LISTING = %w[blog news author].freeze
|
|
9
|
+
POST = %w[post news-article].freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(ctx)
|
|
14
|
+
settings(ctx)
|
|
15
|
+
authors = load_authors(ctx)
|
|
16
|
+
ctx.mds.each { |f| page(ctx, f, authors) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def settings(ctx)
|
|
20
|
+
mtc, errs = ctx.mtc, ctx.errs
|
|
21
|
+
return unless mtc.is_a?(Hash)
|
|
22
|
+
if mtc.key?("blog")
|
|
23
|
+
b = mtc["blog"]
|
|
24
|
+
k0 = "_config.yml: modootoday.blog"
|
|
25
|
+
return errs << "#{k0} must be a map" unless b.is_a?(Hash)
|
|
26
|
+
(b.keys - %w[variant per_page excerpt reading_time related share]).each { |k| errs << "#{k0}: unknown key '#{k}'" }
|
|
27
|
+
errs << "#{k0}.variant must be one of #{VARIANTS.join(', ')}" if b.key?("variant") && !VARIANTS.include?(b["variant"].to_s)
|
|
28
|
+
%w[per_page related].each { |k| errs << "#{k0}.#{k} must be a whole number" if b.key?(k) && !(b[k].is_a?(Integer) && b[k] >= 0) }
|
|
29
|
+
%w[excerpt reading_time share].each { |k| errs << "#{k0}.#{k} must be true or false" if b.key?(k) && ![true, false].include?(b[k]) }
|
|
30
|
+
end
|
|
31
|
+
return unless mtc.key?("news")
|
|
32
|
+
n = mtc["news"]
|
|
33
|
+
ok = n.is_a?(Hash) && (n.keys - %w[sections]).empty? && (!n.key?("sections") || n["sections"].is_a?(Array))
|
|
34
|
+
errs << "_config.yml: modootoday.news must be a map with sections: [category names]" unless ok
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_authors(ctx)
|
|
38
|
+
f = "#{ctx.src}/_data/authors.yml"
|
|
39
|
+
return nil unless File.exist?(f)
|
|
40
|
+
data = (YAML.safe_load(File.read(f)) rescue (ctx.errs << "_data/authors.yml: YAML error"; nil))
|
|
41
|
+
return nil unless data
|
|
42
|
+
return (ctx.errs << "_data/authors.yml must map author ids to {name:, url:, page:, bio:}"; nil) unless data.is_a?(Hash)
|
|
43
|
+
data.each do |id, a|
|
|
44
|
+
next ctx.errs << "_data/authors.yml: #{id} must be a map with name:" unless a.is_a?(Hash) && a["name"].to_s != ""
|
|
45
|
+
ctx.errs << "_data/authors.yml: #{id}.url must be an http(s) URL" if a["url"] && a["url"].to_s !~ %r{\Ahttps?://\S+\z}
|
|
46
|
+
ctx.errs << "_data/authors.yml: #{id}.page must be a site path starting with /" if a["page"] && !a["page"].to_s.start_with?("/")
|
|
47
|
+
end
|
|
48
|
+
data
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def page(ctx, f, authors)
|
|
52
|
+
fm = (Markdown.fm(File.read(f)) rescue nil)
|
|
53
|
+
return unless fm.is_a?(Hash)
|
|
54
|
+
r = ctx.rel(f)
|
|
55
|
+
layout = fm["layout"].to_s
|
|
56
|
+
post = r.split("/").include?("_posts") || POST.include?(layout)
|
|
57
|
+
if LISTING.include?(layout)
|
|
58
|
+
ctx.errs << "#{r}: variant must be one of #{VARIANTS.join(', ')}" if fm.key?("variant") && !VARIANTS.include?(fm["variant"].to_s)
|
|
59
|
+
ctx.errs << "#{r}: news_sections must be a list of category names" if fm.key?("news_sections") && !fm["news_sections"].is_a?(Array)
|
|
60
|
+
end
|
|
61
|
+
ctx.errs << "#{r}: layout: author needs author: <id from _data/authors.yml>" if layout == "author" && fm["author"].to_s.empty?
|
|
62
|
+
ids = Array(fm["authors"] || fm["author"]).map(&:to_s)
|
|
63
|
+
if authors && (post || layout == "author")
|
|
64
|
+
ids.each { |id| ctx.warns << "#{r}: author '#{id}' is not in _data/authors.yml (shown as plain text)" unless authors.key?(id) }
|
|
65
|
+
end
|
|
66
|
+
return unless post
|
|
67
|
+
%w[breaking live featured].each { |k| ctx.errs << "#{r}: #{k} must be true or false" if fm.key?(k) && ![true, false].include?(fm[k]) }
|
|
68
|
+
ctx.errs << "#{r}: sources must be a list" if fm.key?("sources") && !fm["sources"].is_a?(Array)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# 5. Built site: unconverted Markdown markers in HTML, and CSS that GitHub Pages' Ruby Sass
|
|
6
|
+
# passes through or leaves undefined.
|
|
7
|
+
module BuildOutput
|
|
8
|
+
MARKS = /\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/i
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def residue(ctx)
|
|
13
|
+
Dir.glob("#{ctx.site}/**/*.html").each do |f|
|
|
14
|
+
t = File.read(f)
|
|
15
|
+
t = t.gsub(%r{<pre\b.*?</pre>}m, "").gsub(%r{<code\b.*?</code>}m) { |c| c.include?("status:") ? c : "" }
|
|
16
|
+
t.scan(MARKS) { ctx.errs << "#{f}: unconverted alert marker [!#{$1}]" }
|
|
17
|
+
ctx.errs << "#{f}: unconverted status badge (<code>status:)" if t =~ /<code[^>]*>status:/
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def css(ctx)
|
|
22
|
+
errs = ctx.errs
|
|
23
|
+
files = Dir.glob("#{ctx.site}/**/*.css").sort
|
|
24
|
+
defined = {}
|
|
25
|
+
files.each do |f|
|
|
26
|
+
t = File.read(f)
|
|
27
|
+
errs << "#{f}: @use/@forward left in built CSS (Ruby Sass passes them through); use @import" if t =~ /@(use|forward)\s/
|
|
28
|
+
errs << "#{f}: @supports selector() in built CSS" if t =~ /@supports\s+(not\s+)?selector\(/
|
|
29
|
+
errs << "#{f}: custom property with a literal Sass variable (write --x: \#{$var})" if t =~ /--[\w-]+:\s*\$/
|
|
30
|
+
t.scan(/(--mt-[\w-]+)\s*:/) { defined[$1] = true }
|
|
31
|
+
end
|
|
32
|
+
Dir.glob("#{ctx.site}/**/*.html").each { |f| File.read(f).scan(%r{<style[^>]*>(.*?)</style>}m) { $1.scan(/(--mt-[\w-]+)\s*:/) { defined[$1] = true } } }
|
|
33
|
+
files.each do |f|
|
|
34
|
+
File.read(f).scan(/var\((--mt-[\w-]+)/).flatten.uniq.each { |v| errs << "#{f}: var(#{v}) is never defined" unless defined[v] }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# 4b. Registered parts exist in the built HTML of the pages that use the component (needs
|
|
6
|
+
# Nokogiri; skipped without it). Attributes that component scripts add at runtime (role=, aria-*)
|
|
7
|
+
# are checked after scripts run by docs QA.
|
|
8
|
+
module BuiltParts
|
|
9
|
+
RUNTIME_PART = /\[(role|aria-)/
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def nokogiri?
|
|
14
|
+
require "nokogiri"
|
|
15
|
+
true
|
|
16
|
+
rescue LoadError
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(ctx)
|
|
21
|
+
return unless ctx.comps.is_a?(Hash) && nokogiri?
|
|
22
|
+
docs = {}
|
|
23
|
+
ctx.part_uses.each do |c, pages|
|
|
24
|
+
secs = pages.uniq.flat_map do |pg|
|
|
25
|
+
r = pg.delete_suffix(".md")
|
|
26
|
+
cand = File.basename(r) == "index" ? ["#{ctx.site}/#{r}.html"] : ["#{ctx.site}/#{r}.html", "#{ctx.site}/#{r}/index.html"]
|
|
27
|
+
h = cand.find { |x| File.exist?(x) }
|
|
28
|
+
h ? (docs[h] ||= Nokogiri::HTML(File.read(h))).css(%(section[data-c="#{c}"])).to_a : []
|
|
29
|
+
end
|
|
30
|
+
next if secs.empty? # page not built under its source path (custom permalink): nothing to compare
|
|
31
|
+
ctx.comps[c]["parts"].each do |sel|
|
|
32
|
+
next unless sel.is_a?(String) && !sel.match?(RUNTIME_PART)
|
|
33
|
+
found = secs.any? { |sec| (sec.matches?(sel) rescue false) || !(sec.css(sel) rescue []).empty? }
|
|
34
|
+
ctx.errs << "_data/components.yml: #{c}.parts: '#{sel}' not found in the built HTML of #{pages.uniq.sort.join(', ')}" unless found
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# Command line: usage, --help/--version, running the rule groups in order, reporting.
|
|
6
|
+
module CLI
|
|
7
|
+
USAGE = "usage: modootoday-check SRC SITE [--twins] [--github-pages[=VERSION]] [--gh] [--strict] [--schema-recommended] [--i18n] [--i18n-init=LANG,...]"
|
|
8
|
+
# Single source for --help and the README "Commands" table (script/readme-commands, test/project/readme_test.rb).
|
|
9
|
+
OPTIONS = [
|
|
10
|
+
["SRC SITE", "always: front matter, forbidden syntax, alerts, fence info strings, `status:`/`tag:`/`kbd:` values (`since:` is rejected), `sections:` count, `_data/i18n/<lang>.yml` keys against the default language and translated pages against default-language pages, `_data/components.yml` (keys, slot contract counts per section, `parts` against `api.md` and the built HTML), Sass/CSS rules, unconverted markers in the build, schema.org JSON-LD in the build (valid JSON, vocabulary, required properties per type, absolute URLs, ISO 8601 dates, one graph per page, agreement with Open Graph)"],
|
|
11
|
+
["--twins", "every page has a byte-identical Markdown twin, per-language `llms.txt` and `llms-full.txt`"],
|
|
12
|
+
["--github-pages[=VERSION]", "`Gemfile.lock` pins `github-pages` VERSION (default 232)"],
|
|
13
|
+
["--schema-recommended", "also warn about schema.org properties Google Search recommends (author, image, dates, ...) that a JSON-LD node lacks"],
|
|
14
|
+
["--strict", "warnings fail the run"],
|
|
15
|
+
["--i18n", "`_data/i18n/<lang>.yml` against the theme registry (`i18n/en.yml` in the gem): unknown keys, `%{name}` placeholder mismatches, and pages marked `translation: pending`"],
|
|
16
|
+
["--i18n-init=LANG,...", "copy the theme's catalogs (`i18n/<lang>.yml`, 23 languages) into `SRC/_data/i18n/` as a starting point for overrides (optional; the theme already uses them), and list keys an existing catalog lacks"],
|
|
17
|
+
["--gh", "also render each page with `gh api markdown` (network, `gh` login)"],
|
|
18
|
+
["--help, -h", "print usage and options (`--help=markdown` prints the README table)"],
|
|
19
|
+
["--version", "print the rules version"],
|
|
20
|
+
].freeze
|
|
21
|
+
# Rule groups in report order.
|
|
22
|
+
RULES = [Registry, SiteConfig.method(:source_mode), Pages, Layouts, I18n, Registry.method(:i18n_keys), BuiltParts,
|
|
23
|
+
BuildOutput.method(:residue), NewWindow.method(:built), StructuredData, Stylesheets, BuildOutput.method(:css), SiteConfig, Blog, GithubPages, Twins, GhRender].freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def run(argv)
|
|
28
|
+
return help(argv.find { |a| a == "-h" || a.start_with?("--help") }) if argv.any? { |a| a == "-h" || a.start_with?("--help") }
|
|
29
|
+
return (puts "modootoday-check (md-components v1)"; 0) if argv.include?("--version")
|
|
30
|
+
|
|
31
|
+
pos = argv.reject { |a| a.start_with?("--") }
|
|
32
|
+
src, site = pos[0]&.chomp("/"), pos[1]&.chomp("/")
|
|
33
|
+
abort USAGE unless src && site
|
|
34
|
+
ctx = Context.new(src, site, argv)
|
|
35
|
+
RULES.each { |r| r.call(ctx) }
|
|
36
|
+
report(ctx)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def help(h)
|
|
40
|
+
if h == "--help=markdown"
|
|
41
|
+
puts "| Option | Checks |", "|---|---|"
|
|
42
|
+
OPTIONS.each { |o, d| puts "| `#{o}` | #{d.gsub("|", "\\|")} |" }
|
|
43
|
+
else
|
|
44
|
+
puts USAGE, "", "options:"
|
|
45
|
+
OPTIONS.each { |o, d| puts format(" %-26s %s", o, d.delete("`")) }
|
|
46
|
+
puts "", "exit status: 0 no problems, 1 problems found or bad usage"
|
|
47
|
+
end
|
|
48
|
+
0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def report(ctx)
|
|
52
|
+
errs, warns = ctx.errs, ctx.warns
|
|
53
|
+
errs.concat(warns.map { |w| "#{w} [strict]" }) if ctx.option?("--strict")
|
|
54
|
+
warns.each { |w| warn "warning: #{w}" } unless ctx.option?("--strict")
|
|
55
|
+
puts errs
|
|
56
|
+
puts errs.empty? ? "md-components check: OK (#{ctx.mds.size} pages)" : "md-components check: #{errs.size} problem(s)"
|
|
57
|
+
errs.empty? ? 0 : 1
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# Values the transforms need (Rule 2b): inline `file:`, `progress:`, `embed:` code spans, and per-section
|
|
6
|
+
# content rules for c: gallery, quote, pricing and subscribe that slot counts cannot express.
|
|
7
|
+
module ComponentRules
|
|
8
|
+
SKELETONS = %w[text card image avatar row].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def inline(ctx, r, b)
|
|
13
|
+
errs, lines = ctx.errs, Markdown.strip_fences(b).lines
|
|
14
|
+
text = lines.join
|
|
15
|
+
text.scan(/`progress:([^`]*)`/) do |(v)|
|
|
16
|
+
errs << "#{r}: `progress:#{v}` must be a whole number from 0 to 100" unless v =~ /\A(100|[1-9]?\d)\z/
|
|
17
|
+
end
|
|
18
|
+
text.scan(/`skeleton:([^`]*)`/) do |(v)|
|
|
19
|
+
errs << "#{r}: `skeleton:#{v}` must be one of #{SKELETONS.join(', ')}" unless SKELETONS.include?(v)
|
|
20
|
+
end
|
|
21
|
+
text.scan(/`embed:([^`]*)`/) do |(v)|
|
|
22
|
+
errs << "#{r}: `embed:#{v}` must be an https:// URL, optionally followed by a title" unless v =~ %r{\Ahttps://[^\s"<>]+( [^"<>]+)?\z}
|
|
23
|
+
end
|
|
24
|
+
rl = b.lines
|
|
25
|
+
rl.each_with_index do |l, i|
|
|
26
|
+
next unless (fm = l.strip.match(/\A`file:([^`]*)`\z/))
|
|
27
|
+
nxt = rl[(i + 1)..].to_a.find { |x| x.strip != "" }.to_s
|
|
28
|
+
errs << "#{r}: `file:#{fm[1]}` must be alone in a paragraph right before a fenced code block" unless nxt =~ /\A\s*(```|~~~)/ && fm[1] !~ /\s/
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def section(ctx, r, i, s, md)
|
|
33
|
+
errs, (m, _raw) = ctx.errs, md || ["", ""]
|
|
34
|
+
w = "#{r}: sections[#{i}] #{s['c']}"
|
|
35
|
+
case s["c"]
|
|
36
|
+
when "gallery"
|
|
37
|
+
imgs = m.scan(/^[-*+]\s+(.*)$/).flatten
|
|
38
|
+
errs << "#{w}: each list item must be one image  with alt text" unless imgs.all? { |x| x.strip =~ /\A!\[[^\]\n]*\S[^\]\n]*\]\([^)\s]+\)\z/ }
|
|
39
|
+
errs << "#{w}: write the image list without blank lines between items" if m =~ /^[-*+]\s.*\n\s*\n[-*+]\s/
|
|
40
|
+
when "quote"
|
|
41
|
+
errs << "#{w}: needs a > quote whose last line is the attribution (> — Name)" unless m =~ /^>\s*— \S/
|
|
42
|
+
when "pricing"
|
|
43
|
+
n = m.scan(/^###(?: |$)/).size
|
|
44
|
+
rec = s["recommended"]
|
|
45
|
+
errs << "#{w}: recommended: must be a plan number from 1 to #{n}" if s.key?("recommended") && !(rec.is_a?(Integer) && rec.between?(1, n))
|
|
46
|
+
when "subscribe"
|
|
47
|
+
act = ctx.mtc.is_a?(Hash) && ctx.mtc.dig("subscribe", "action")
|
|
48
|
+
ctx.warns << "#{w}: no form is shown until _config.yml sets modootoday.subscribe.action" unless act
|
|
49
|
+
errs << "#{w}: modootoday.subscribe.action must be an https:// URL" if act && act.to_s !~ %r{\Ahttps://\S+\z}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modootoday
|
|
4
|
+
module Check
|
|
5
|
+
# What every rule reads (source and site paths, options, parsed config and data) and the
|
|
6
|
+
# error/warning lists every rule writes to. Loaded once per run.
|
|
7
|
+
class Context
|
|
8
|
+
SKIP = %r{(^|/)(_site|_includes|_layouts|_data|vendor|node_modules)/}
|
|
9
|
+
|
|
10
|
+
attr_reader :src, :site, :argv, :errs, :warns, :mds, :comps, :i18n, :site_cfg, :mtc
|
|
11
|
+
attr_accessor :part_uses
|
|
12
|
+
|
|
13
|
+
def initialize(src, site, argv)
|
|
14
|
+
@src, @site, @argv = src, site, argv
|
|
15
|
+
@errs, @warns = [], []
|
|
16
|
+
@part_uses = {} # component -> source pages that use it (parts are looked up in their built HTML)
|
|
17
|
+
comps_file = "#{src}/_data/components.yml"
|
|
18
|
+
@comps = File.exist?(comps_file) ? (YAML.safe_load(File.read(comps_file)) || {}) : nil
|
|
19
|
+
@i18n = Dir.glob("#{src}/_data/i18n/*.yml").to_h { |f| [File.basename(f, ".yml"), YAML.safe_load(File.read(f)) || {}] }
|
|
20
|
+
cfg_file = "#{src}/_config.yml"
|
|
21
|
+
@site_cfg = (File.exist?(cfg_file) && ((YAML.safe_load(File.read(cfg_file), permitted_classes: [Date, Time]) rescue nil) || {})) || {}
|
|
22
|
+
@mtc = @site_cfg["modootoday"] || {}
|
|
23
|
+
# directories listed in the site's exclude: are not pages (e.g. a nested example site)
|
|
24
|
+
excl = Array(@site_cfg["exclude"]).map { |e| e.to_s.chomp("/") }.select { |e| File.directory?("#{src}/#{e}") }
|
|
25
|
+
@mds = Dir.glob("#{src}/**/*.md").reject { |f| f.delete_prefix(src) =~ SKIP }.sort
|
|
26
|
+
@mds.reject! { |f| excl.any? { |e| f.start_with?("#{src}/#{e}/") } }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def option?(name) = argv.include?(name)
|
|
30
|
+
def rel(f) = f.delete_prefix("#{src}/")
|
|
31
|
+
def skipped?(f) = f.delete_prefix(src) =~ SKIP
|
|
32
|
+
|
|
33
|
+
# modootoday.twins: source = front-matter-less mode: a page without front matter is rendered by
|
|
34
|
+
# jekyll-optional-front-matter AND copied raw to its own path, which then is its twin.
|
|
35
|
+
def source_mode? = mtc.is_a?(Hash) && mtc["twins"] == "source"
|
|
36
|
+
|
|
37
|
+
# The language of a page path ("ko/guide" -> "ko") when that language has an i18n file.
|
|
38
|
+
def lang_of(r) = (r.split("/").first if r.include?("/") && i18n.key?(r.split("/").first))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Modootoday
|
|
6
|
+
module Check
|
|
7
|
+
# --gh: render each page with `gh api markdown` (needs gh auth and network).
|
|
8
|
+
module GhRender
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def call(ctx)
|
|
12
|
+
return unless ctx.option?("--gh")
|
|
13
|
+
ctx.mds.each do |f|
|
|
14
|
+
out, st = Open3.capture2("gh", "api", "markdown", "-f", "mode=gfm", "-f", "text=#{Markdown.body(File.read(f))}")
|
|
15
|
+
next ctx.errs << "#{ctx.rel(f)}: gh api failed" unless st.success?
|
|
16
|
+
ctx.errs << "#{ctx.rel(f)}: GitHub shows ': ' definition marker" if out =~ /<p>: /
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|