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.
Files changed (352) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +182 -0
  3. data/LICENSE +21 -0
  4. data/README.ko.md +115 -0
  5. data/README.md +112 -0
  6. data/RULES.md +120 -0
  7. data/_includes/footer-custom.html +5 -0
  8. data/_includes/head-custom.html +1 -0
  9. data/_includes/modootoday/actions.html +17 -0
  10. data/_includes/modootoday/archive-years.html +30 -0
  11. data/_includes/modootoday/archive.html +44 -0
  12. data/_includes/modootoday/blog/card.html +23 -0
  13. data/_includes/modootoday/blog/config.html +17 -0
  14. data/_includes/modootoday/blog/list.html +13 -0
  15. data/_includes/modootoday/blog/pager.html +15 -0
  16. data/_includes/modootoday/blog/reading-time.html +10 -0
  17. data/_includes/modootoday/blog/select.html +18 -0
  18. data/_includes/modootoday/brand/icon.html +39 -0
  19. data/_includes/modootoday/brand/logo.html +7 -0
  20. data/_includes/modootoday/breadcrumb.html +22 -0
  21. data/_includes/modootoday/config.html +41 -0
  22. data/_includes/modootoday/detail/cover.html +5 -0
  23. data/_includes/modootoday/detail/cta.html +13 -0
  24. data/_includes/modootoday/detail/facts.html +31 -0
  25. data/_includes/modootoday/detail/jsonld.html +34 -0
  26. data/_includes/modootoday/directory/date-tile.html +2 -0
  27. data/_includes/modootoday/directory/filter.html +7 -0
  28. data/_includes/modootoday/directory/ics.html +17 -0
  29. data/_includes/modootoday/directory/item.html +18 -0
  30. data/_includes/modootoday/directory/items.html +4 -0
  31. data/_includes/modootoday/directory/jsonld.html +5 -0
  32. data/_includes/modootoday/directory/list.html +16 -0
  33. data/_includes/modootoday/directory/values.html +6 -0
  34. data/_includes/modootoday/doc/page-actions.html +15 -0
  35. data/_includes/modootoday/doc/pagination.html +24 -0
  36. data/_includes/modootoday/doc/sidebar-item.html +11 -0
  37. data/_includes/modootoday/doc/sidebar.html +28 -0
  38. data/_includes/modootoday/doc/twin.html +8 -0
  39. data/_includes/modootoday/docs-extra/head.html +7 -0
  40. data/_includes/modootoday/docs-extra/jsonld.html +19 -0
  41. data/_includes/modootoday/head-meta.html +90 -0
  42. data/_includes/modootoday/home-url.html +4 -0
  43. data/_includes/modootoday/i18n/ar.html +2 -0
  44. data/_includes/modootoday/i18n/de.html +2 -0
  45. data/_includes/modootoday/i18n/el.html +2 -0
  46. data/_includes/modootoday/i18n/es.html +2 -0
  47. data/_includes/modootoday/i18n/fa.html +2 -0
  48. data/_includes/modootoday/i18n/fr.html +2 -0
  49. data/_includes/modootoday/i18n/id.html +2 -0
  50. data/_includes/modootoday/i18n/it.html +2 -0
  51. data/_includes/modootoday/i18n/ja.html +2 -0
  52. data/_includes/modootoday/i18n/ko.html +2 -0
  53. data/_includes/modootoday/i18n/ml.html +2 -0
  54. data/_includes/modootoday/i18n/name.html +2 -0
  55. data/_includes/modootoday/i18n/pl.html +2 -0
  56. data/_includes/modootoday/i18n/pt-BR.html +2 -0
  57. data/_includes/modootoday/i18n/ro.html +2 -0
  58. data/_includes/modootoday/i18n/ru.html +2 -0
  59. data/_includes/modootoday/i18n/th.html +2 -0
  60. data/_includes/modootoday/i18n/tr.html +2 -0
  61. data/_includes/modootoday/i18n/uk.html +2 -0
  62. data/_includes/modootoday/i18n/ur-Latn.html +2 -0
  63. data/_includes/modootoday/i18n/vi.html +2 -0
  64. data/_includes/modootoday/i18n/zh-CN.html +2 -0
  65. data/_includes/modootoday/i18n/zh-TW.html +2 -0
  66. data/_includes/modootoday/i18n.html +163 -0
  67. data/_includes/modootoday/jsonld/article.html +15 -0
  68. data/_includes/modootoday/jsonld/author.html +8 -0
  69. data/_includes/modootoday/jsonld/breadcrumb.html +17 -0
  70. data/_includes/modootoday/jsonld/entity.html +16 -0
  71. data/_includes/modootoday/jsonld/graph.html +24 -0
  72. data/_includes/modootoday/jsonld/profile.html +20 -0
  73. data/_includes/modootoday/jsonld/props.html +13 -0
  74. data/_includes/modootoday/jsonld/publisher.html +11 -0
  75. data/_includes/modootoday/jsonld/type.html +29 -0
  76. data/_includes/modootoday/jsonld/webpage.html +17 -0
  77. data/_includes/modootoday/jsonld/website.html +9 -0
  78. data/_includes/modootoday/lang-switch.html +31 -0
  79. data/_includes/modootoday/link.html +3 -0
  80. data/_includes/modootoday/links/attrs.html +9 -0
  81. data/_includes/modootoday/links/content.html +16 -0
  82. data/_includes/modootoday/markdown/c/details.html +6 -0
  83. data/_includes/modootoday/markdown/c/faq.html +2 -0
  84. data/_includes/modootoday/markdown/c/filetree.html +3 -0
  85. data/_includes/modootoday/markdown/c/gallery.html +5 -0
  86. data/_includes/modootoday/markdown/c/linkcards.html +2 -0
  87. data/_includes/modootoday/markdown/c/params.html +3 -0
  88. data/_includes/modootoday/markdown/c/pricing.html +10 -0
  89. data/_includes/modootoday/markdown/c/quote.html +6 -0
  90. data/_includes/modootoday/markdown/c/subscribe.html +6 -0
  91. data/_includes/modootoday/markdown/components.html +114 -0
  92. data/_includes/modootoday/markdown/inline.html +46 -0
  93. data/_includes/modootoday/markdown/llms-pages.html +14 -0
  94. data/_includes/modootoday/markdown/raw-body.html +6 -0
  95. data/_includes/modootoday/markdown/sections.html +43 -0
  96. data/_includes/modootoday/marketing/head.html +6 -0
  97. data/_includes/modootoday/meta/byline.html +24 -0
  98. data/_includes/modootoday/meta/post-nav.html +16 -0
  99. data/_includes/modootoday/meta/related.html +20 -0
  100. data/_includes/modootoday/meta/series.html +11 -0
  101. data/_includes/modootoday/meta/share.html +4 -0
  102. data/_includes/modootoday/nav.html +19 -0
  103. data/_includes/modootoday/news/badge.html +4 -0
  104. data/_includes/modootoday/news/front.html +30 -0
  105. data/_includes/modootoday/news/notes.html +10 -0
  106. data/_includes/modootoday/page-meta.html +16 -0
  107. data/_includes/modootoday/pages/error.html +13 -0
  108. data/_includes/modootoday/pages/hero/scene.html +27 -0
  109. data/_includes/modootoday/pages/hero/sparks.html +6 -0
  110. data/_includes/modootoday/pages/hero.html +11 -0
  111. data/_includes/modootoday/pages/search-form.html +14 -0
  112. data/_includes/modootoday/search.json +11 -0
  113. data/_includes/modootoday/stylesheets.html +36 -0
  114. data/_includes/modootoday/t.html +7 -0
  115. data/_includes/modootoday/theme-color.html +19 -0
  116. data/_includes/modootoday/toc.html +17 -0
  117. data/_includes/modootoday/tokens.html +11 -0
  118. data/_includes/sections/showcase.html +18 -0
  119. data/_layouts/archive.html +6 -0
  120. data/_layouts/author.html +19 -0
  121. data/_layouts/blog.html +28 -0
  122. data/_layouts/book.html +21 -0
  123. data/_layouts/brand.html +21 -0
  124. data/_layouts/case-study.html +5 -0
  125. data/_layouts/changelog.html +24 -0
  126. data/_layouts/contact.html +27 -0
  127. data/_layouts/course.html +21 -0
  128. data/_layouts/default.html +87 -0
  129. data/_layouts/detail.html +31 -0
  130. data/_layouts/directory.html +12 -0
  131. data/_layouts/doc.html +27 -0
  132. data/_layouts/download.html +21 -0
  133. data/_layouts/episode.html +5 -0
  134. data/_layouts/error.html +7 -0
  135. data/_layouts/event.html +5 -0
  136. data/_layouts/events.html +6 -0
  137. data/_layouts/feature.html +18 -0
  138. data/_layouts/feed.html +35 -0
  139. data/_layouts/glossary.html +24 -0
  140. data/_layouts/home.html +7 -0
  141. data/_layouts/hub.html +16 -0
  142. data/_layouts/integrations.html +7 -0
  143. data/_layouts/job.html +5 -0
  144. data/_layouts/jobs.html +7 -0
  145. data/_layouts/legal.html +21 -0
  146. data/_layouts/links.html +18 -0
  147. data/_layouts/llms-full.html +8 -0
  148. data/_layouts/llms.html +32 -0
  149. data/_layouts/minimal.html +8 -0
  150. data/_layouts/news-article.html +6 -0
  151. data/_layouts/news.html +22 -0
  152. data/_layouts/portfolio.html +7 -0
  153. data/_layouts/post.html +54 -0
  154. data/_layouts/pricing.html +32 -0
  155. data/_layouts/project.html +5 -0
  156. data/_layouts/redirect.html +21 -0
  157. data/_layouts/reference.html +24 -0
  158. data/_layouts/resume.html +50 -0
  159. data/_layouts/roadmap.html +22 -0
  160. data/_layouts/search.html +8 -0
  161. data/_layouts/sponsors.html +7 -0
  162. data/_layouts/team.html +7 -0
  163. data/_layouts/twin.html +2 -0
  164. data/_layouts/wide.html +7 -0
  165. data/_sass/modootoday/_access.scss +12 -0
  166. data/_sass/modootoday/_core.scss +18 -0
  167. data/_sass/modootoday/_settings.scss +5 -0
  168. data/_sass/modootoday/_sheet.scss +6 -0
  169. data/_sass/modootoday/base/_bidi.scss +8 -0
  170. data/_sass/modootoday/base/_code.scss +18 -0
  171. data/_sass/modootoday/base/_interaction.scss +6 -0
  172. data/_sass/modootoday/base/_prose.scss +32 -0
  173. data/_sass/modootoday/base/_reset.scss +14 -0
  174. data/_sass/modootoday/base/_table.scss +8 -0
  175. data/_sass/modootoday/blog/_article.scss +27 -0
  176. data/_sass/modootoday/blog/_cards.scss +36 -0
  177. data/_sass/modootoday/blog/_index.scss +10 -0
  178. data/_sass/modootoday/blog/_news.scss +26 -0
  179. data/_sass/modootoday/blog/_post.scss +9 -0
  180. data/_sass/modootoday/components/core/chrome/_banner.scss +7 -0
  181. data/_sass/modootoday/components/core/chrome/_breadcrumb.scss +10 -0
  182. data/_sass/modootoday/components/core/chrome/_nav.scss +39 -0
  183. data/_sass/modootoday/components/core/chrome/_pagination.scss +10 -0
  184. data/_sass/modootoday/components/core/chrome/_theme-toggle.scss +16 -0
  185. data/_sass/modootoday/components/core/inline/_badge.scss +15 -0
  186. data/_sass/modootoday/components/core/inline/_callout.scss +14 -0
  187. data/_sass/modootoday/components/core/inline/_code-copy.scss +11 -0
  188. data/_sass/modootoday/components/core/inline/_code.scss +11 -0
  189. data/_sass/modootoday/components/core/inline/_cta.scss +11 -0
  190. data/_sass/modootoday/components/core/inline/_embed.scss +6 -0
  191. data/_sass/modootoday/components/core/inline/_figure.scss +9 -0
  192. data/_sass/modootoday/components/core/inline/_footnotes.scss +6 -0
  193. data/_sass/modootoday/components/core/inline/_hero.scss +8 -0
  194. data/_sass/modootoday/components/core/inline/_kbd.scss +6 -0
  195. data/_sass/modootoday/components/core/inline/_progress.scss +8 -0
  196. data/_sass/modootoday/components/doc/_page-actions.scss +10 -0
  197. data/_sass/modootoday/components/doc/_sidebar.scss +31 -0
  198. data/_sass/modootoday/components/doc/_toc.scss +19 -0
  199. data/_sass/modootoday/components/loading/_loading.scss +33 -0
  200. data/_sass/modootoday/components/search/_search.scss +36 -0
  201. data/_sass/modootoday/components/section/showcase/_card.scss +15 -0
  202. data/_sass/modootoday/components/section/showcase/_cta.scss +7 -0
  203. data/_sass/modootoday/components/section/showcase/_gallery.scss +15 -0
  204. data/_sass/modootoday/components/section/showcase/_linkcards.scss +12 -0
  205. data/_sass/modootoday/components/section/showcase/_pricing.scss +12 -0
  206. data/_sass/modootoday/components/section/showcase/_quote.scss +7 -0
  207. data/_sass/modootoday/components/section/showcase/_subscribe.scss +9 -0
  208. data/_sass/modootoday/components/section/structure/_accordion.scss +15 -0
  209. data/_sass/modootoday/components/section/structure/_details.scss +15 -0
  210. data/_sass/modootoday/components/section/structure/_faq.scss +5 -0
  211. data/_sass/modootoday/components/section/structure/_filetree.scss +13 -0
  212. data/_sass/modootoday/components/section/structure/_params.scss +8 -0
  213. data/_sass/modootoday/components/section/structure/_steps.scss +13 -0
  214. data/_sass/modootoday/components/section/structure/_tabs.scss +21 -0
  215. data/_sass/modootoday/components/section/structure/_timeline.scss +10 -0
  216. data/_sass/modootoday/detail/_index.scss +8 -0
  217. data/_sass/modootoday/detail/_page.scss +25 -0
  218. data/_sass/modootoday/directory/_filter.scss +12 -0
  219. data/_sass/modootoday/directory/_grid.scss +18 -0
  220. data/_sass/modootoday/directory/_index.scss +11 -0
  221. data/_sass/modootoday/directory/_presets.scss +16 -0
  222. data/_sass/modootoday/docs-extra/_index.scss +7 -0
  223. data/_sass/modootoday/docs-extra/_page.scss +37 -0
  224. data/_sass/modootoday/hero/_index.scss +6 -0
  225. data/_sass/modootoday/hero/_layout.scss +28 -0
  226. data/_sass/modootoday/hero/_scene.scss +20 -0
  227. data/_sass/modootoday/kit/_button.scss +8 -0
  228. data/_sass/modootoday/layout/_page-meta.scss +9 -0
  229. data/_sass/modootoday/layout/_safe-area.scss +9 -0
  230. data/_sass/modootoday/layout/_shell.scss +56 -0
  231. data/_sass/modootoday/marketing/_forms.scss +11 -0
  232. data/_sass/modootoday/marketing/_index.scss +10 -0
  233. data/_sass/modootoday/marketing/_page.scss +31 -0
  234. data/_sass/modootoday/marketing/_pricing.scss +15 -0
  235. data/_sass/modootoday/pages/_actions.scss +11 -0
  236. data/_sass/modootoday/pages/_error.scss +7 -0
  237. data/_sass/modootoday/pages/_index.scss +11 -0
  238. data/_sass/modootoday/pages/_legal.scss +17 -0
  239. data/_sass/modootoday/pages/_search.scss +9 -0
  240. data/_sass/modootoday/pages/_shells.scss +11 -0
  241. data/_sass/modootoday/resume/_index.scss +5 -0
  242. data/_sass/modootoday/resume/_page.scss +27 -0
  243. data/_sass/modootoday/tokens/_emit.scss +15 -0
  244. data/_sass/modootoday/tokens/_primitive.scss +18 -0
  245. data/_sass/modootoday/tokens/_semantic.scss +37 -0
  246. data/_sass/modootoday/tools/_functions.scss +2 -0
  247. data/_sass/modootoday/tools/_img-dark.scss +12 -0
  248. data/_sass/modootoday/tools/_mixins.scss +8 -0
  249. data/_sass/modootoday.scss +40 -0
  250. data/assets/css/modootoday/doc.scss +10 -0
  251. data/assets/css/modootoday/loading.scss +8 -0
  252. data/assets/css/modootoday/search.scss +8 -0
  253. data/assets/css/modootoday/showcase/card.scss +7 -0
  254. data/assets/css/modootoday/showcase/cta.scss +7 -0
  255. data/assets/css/modootoday/showcase/gallery.scss +7 -0
  256. data/assets/css/modootoday/showcase/linkcards.scss +7 -0
  257. data/assets/css/modootoday/showcase/pricing.scss +7 -0
  258. data/assets/css/modootoday/showcase/quote.scss +7 -0
  259. data/assets/css/modootoday/showcase/subscribe.scss +7 -0
  260. data/assets/css/modootoday/structure/accordion.scss +7 -0
  261. data/assets/css/modootoday/structure/details.scss +7 -0
  262. data/assets/css/modootoday/structure/faq.scss +7 -0
  263. data/assets/css/modootoday/structure/filetree.scss +7 -0
  264. data/assets/css/modootoday/structure/params.scss +7 -0
  265. data/assets/css/modootoday/structure/steps.scss +7 -0
  266. data/assets/css/modootoday/structure/tabs.scss +7 -0
  267. data/assets/css/modootoday/structure/timeline.scss +7 -0
  268. data/assets/css/modootoday-blog.scss +7 -0
  269. data/assets/css/modootoday-detail.scss +7 -0
  270. data/assets/css/modootoday-directory.scss +7 -0
  271. data/assets/css/modootoday-docs-extra.scss +7 -0
  272. data/assets/css/modootoday-hero.scss +7 -0
  273. data/assets/css/modootoday-marketing.scss +7 -0
  274. data/assets/css/modootoday-pages.scss +7 -0
  275. data/assets/css/modootoday-resume.scss +7 -0
  276. data/assets/css/modootoday.scss +17 -0
  277. data/assets/js/modootoday/accordion.js +13 -0
  278. data/assets/js/modootoday/code-copy.js +29 -0
  279. data/assets/js/modootoday/menu.js +19 -0
  280. data/assets/js/modootoday/scroll-region.js +14 -0
  281. data/assets/js/modootoday/search-keys.js +18 -0
  282. data/assets/js/modootoday/search.js +31 -0
  283. data/assets/js/modootoday/share.js +16 -0
  284. data/assets/js/modootoday/tabs.js +26 -0
  285. data/assets/js/modootoday/theme-init.js +2 -0
  286. data/assets/js/modootoday/theme-toggle.js +17 -0
  287. data/assets/modootoday/apple-touch-icon.png +0 -0
  288. data/assets/modootoday/favicon-32.png +0 -0
  289. data/assets/modootoday/favicon.svg +1 -0
  290. data/assets/modootoday/hero/dawn-1280.webp +0 -0
  291. data/assets/modootoday/hero/dawn-640.webp +0 -0
  292. data/assets/modootoday/hero/dawn-960.webp +0 -0
  293. data/exe/modootoday-check +7 -0
  294. data/i18n/ar.yml +130 -0
  295. data/i18n/de.yml +130 -0
  296. data/i18n/el.yml +130 -0
  297. data/i18n/en.yml +131 -0
  298. data/i18n/es.yml +130 -0
  299. data/i18n/fa.yml +130 -0
  300. data/i18n/fr.yml +130 -0
  301. data/i18n/glossary.yml +61 -0
  302. data/i18n/id.yml +130 -0
  303. data/i18n/it.yml +130 -0
  304. data/i18n/ja.yml +130 -0
  305. data/i18n/ko.yml +130 -0
  306. data/i18n/languages.yml +25 -0
  307. data/i18n/ml.yml +130 -0
  308. data/i18n/pl.yml +130 -0
  309. data/i18n/pt-BR.yml +130 -0
  310. data/i18n/ro.yml +130 -0
  311. data/i18n/ru.yml +130 -0
  312. data/i18n/th.yml +130 -0
  313. data/i18n/tr.yml +130 -0
  314. data/i18n/uk.yml +130 -0
  315. data/i18n/ur-Latn.yml +130 -0
  316. data/i18n/vi.yml +130 -0
  317. data/i18n/zh-CN.yml +130 -0
  318. data/i18n/zh-TW.yml +130 -0
  319. data/lib/jekyll-theme-modootoday.rb +59 -0
  320. data/lib/modootoday/check/a11y/content.rb +57 -0
  321. data/lib/modootoday/check/a11y/new_window.rb +64 -0
  322. data/lib/modootoday/check/blog.rb +72 -0
  323. data/lib/modootoday/check/build_output.rb +39 -0
  324. data/lib/modootoday/check/built_parts.rb +40 -0
  325. data/lib/modootoday/check/cli.rb +61 -0
  326. data/lib/modootoday/check/component_rules.rb +54 -0
  327. data/lib/modootoday/check/context.rb +41 -0
  328. data/lib/modootoday/check/gh_render.rb +21 -0
  329. data/lib/modootoday/check/github_pages.rb +23 -0
  330. data/lib/modootoday/check/i18n.rb +92 -0
  331. data/lib/modootoday/check/layouts/detail.rb +52 -0
  332. data/lib/modootoday/check/layouts/directory.rb +36 -0
  333. data/lib/modootoday/check/layouts/docs_extra.rb +84 -0
  334. data/lib/modootoday/check/layouts/marketing.rb +64 -0
  335. data/lib/modootoday/check/layouts/shape.rb +38 -0
  336. data/lib/modootoday/check/layouts/shells.rb +47 -0
  337. data/lib/modootoday/check/layouts.rb +37 -0
  338. data/lib/modootoday/check/markdown.rb +39 -0
  339. data/lib/modootoday/check/page_sections.rb +72 -0
  340. data/lib/modootoday/check/page_syntax.rb +104 -0
  341. data/lib/modootoday/check/page_values.rb +30 -0
  342. data/lib/modootoday/check/pages.rb +31 -0
  343. data/lib/modootoday/check/registry.rb +69 -0
  344. data/lib/modootoday/check/site_config.rb +74 -0
  345. data/lib/modootoday/check/slots.rb +50 -0
  346. data/lib/modootoday/check/structured_data.rb +97 -0
  347. data/lib/modootoday/check/stylesheets.rb +28 -0
  348. data/lib/modootoday/check/twins.rb +69 -0
  349. data/lib/modootoday/check.rb +22 -0
  350. data/lib/modootoday/schema/rules.rb +49 -0
  351. data/lib/modootoday/schema/vocab.txt +2478 -0
  352. 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