aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/__init__.py21
-rw-r--r--src/extensions/__init__.py19
-rw-r--r--src/extensions/emoji.py98
-rw-r--r--src/overrides/assets/javascripts/components/_/index.ts104
-rw-r--r--src/overrides/assets/javascripts/components/iconsearch/_/index.ts94
-rw-r--r--src/overrides/assets/javascripts/components/iconsearch/index.ts25
-rw-r--r--src/overrides/assets/javascripts/components/iconsearch/query/index.ts96
-rw-r--r--src/overrides/assets/javascripts/components/iconsearch/result/index.ts237
-rw-r--r--src/overrides/assets/javascripts/components/index.ts25
-rw-r--r--src/overrides/assets/javascripts/components/sponsorship/index.ts149
-rw-r--r--src/overrides/assets/javascripts/custom.ts55
-rw-r--r--src/overrides/assets/javascripts/integrations/analytics/index.ts42
-rw-r--r--src/overrides/assets/javascripts/integrations/index.ts23
-rw-r--r--src/overrides/assets/javascripts/templates/iconsearch/index.tsx95
-rw-r--r--src/overrides/assets/javascripts/templates/index.ts24
-rw-r--r--src/overrides/assets/javascripts/templates/sponsorship/index.tsx67
-rw-r--r--src/overrides/assets/stylesheets/custom.scss44
-rw-r--r--src/overrides/assets/stylesheets/custom/_typeset.scss294
-rw-r--r--src/overrides/assets/stylesheets/custom/layout/_banner.scss66
-rw-r--r--src/overrides/assets/stylesheets/custom/layout/_hero.scss123
-rw-r--r--src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss136
-rw-r--r--src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss128
-rw-r--r--src/overrides/home.html106
-rw-r--r--src/overrides/hooks/shortcodes.py283
-rw-r--r--src/overrides/hooks/translations.html54
-rw-r--r--src/overrides/hooks/translations.py193
-rw-r--r--src/overrides/main.html59
-rw-r--r--src/plugins/__init__.py19
-rw-r--r--src/plugins/blog/__init__.py19
-rw-r--r--src/plugins/blog/author.py38
-rw-r--r--src/plugins/blog/config.py88
-rw-r--r--src/plugins/blog/plugin.py884
-rw-r--r--src/plugins/blog/readtime/__init__.py51
-rw-r--r--src/plugins/blog/readtime/parser.py45
-rw-r--r--src/plugins/blog/structure/__init__.py292
-rw-r--r--src/plugins/blog/structure/config.py37
-rw-r--r--src/plugins/blog/structure/markdown.py58
-rw-r--r--src/plugins/blog/structure/options.py87
-rw-r--r--src/plugins/blog/templates/__init__.py42
-rw-r--r--src/plugins/group/__init__.py19
-rw-r--r--src/plugins/group/config.py33
-rw-r--r--src/plugins/group/plugin.py151
-rw-r--r--src/plugins/info/__init__.py19
-rw-r--r--src/plugins/info/config.py35
-rw-r--r--src/plugins/info/plugin.py245
-rw-r--r--src/plugins/offline/__init__.py19
-rw-r--r--src/plugins/offline/config.py30
-rw-r--r--src/plugins/offline/plugin.py69
-rw-r--r--src/plugins/search/__init__.py19
-rw-r--r--src/plugins/search/config.py58
-rw-r--r--src/plugins/search/plugin.py580
-rw-r--r--src/plugins/social/__init__.py19
-rw-r--r--src/plugins/social/config.py48
-rw-r--r--src/plugins/social/plugin.py516
-rw-r--r--src/plugins/tags/__init__.py27
-rw-r--r--src/plugins/tags/config.py38
-rw-r--r--src/plugins/tags/plugin.py182
-rw-r--r--src/templates/.icons/logo.afdesignbin0 -> 31465 bytes
-rw-r--r--src/templates/.icons/logo.svg6
-rw-r--r--src/templates/404.html28
-rw-r--r--src/templates/__init__.py19
-rw-r--r--src/templates/assets/images/favicon.pngbin0 -> 1870 bytes
-rw-r--r--src/templates/assets/javascripts/_/index.ts148
-rw-r--r--src/templates/assets/javascripts/browser/document/index.ts48
-rw-r--r--src/templates/assets/javascripts/browser/element/_/.eslintrc6
-rw-r--r--src/templates/assets/javascripts/browser/element/_/index.ts120
-rw-r--r--src/templates/assets/javascripts/browser/element/focus/index.ts81
-rw-r--r--src/templates/assets/javascripts/browser/element/index.ts27
-rw-r--r--src/templates/assets/javascripts/browser/element/offset/_/index.ts86
-rw-r--r--src/templates/assets/javascripts/browser/element/offset/content/index.ts76
-rw-r--r--src/templates/assets/javascripts/browser/element/offset/index.ts24
-rw-r--r--src/templates/assets/javascripts/browser/element/size/_/index.ts151
-rw-r--r--src/templates/assets/javascripts/browser/element/size/content/index.ts67
-rw-r--r--src/templates/assets/javascripts/browser/element/size/index.ts24
-rw-r--r--src/templates/assets/javascripts/browser/element/visibility/index.ts131
-rw-r--r--src/templates/assets/javascripts/browser/index.ts32
-rw-r--r--src/templates/assets/javascripts/browser/keyboard/index.ts148
-rw-r--r--src/templates/assets/javascripts/browser/location/_/index.ts85
-rw-r--r--src/templates/assets/javascripts/browser/location/hash/index.ts104
-rw-r--r--src/templates/assets/javascripts/browser/location/index.ts24
-rw-r--r--src/templates/assets/javascripts/browser/media/index.ts95
-rw-r--r--src/templates/assets/javascripts/browser/request/index.ts141
-rw-r--r--src/templates/assets/javascripts/browser/script/index.ts70
-rw-r--r--src/templates/assets/javascripts/browser/toggle/index.ts102
-rw-r--r--src/templates/assets/javascripts/browser/viewport/_/index.ts69
-rw-r--r--src/templates/assets/javascripts/browser/viewport/at/index.ts84
-rw-r--r--src/templates/assets/javascripts/browser/viewport/index.ts26
-rw-r--r--src/templates/assets/javascripts/browser/viewport/offset/index.ts78
-rw-r--r--src/templates/assets/javascripts/browser/viewport/size/index.ts71
-rw-r--r--src/templates/assets/javascripts/browser/worker/index.ts112
-rw-r--r--src/templates/assets/javascripts/bundle.ts316
-rw-r--r--src/templates/assets/javascripts/components/_/index.ts138
-rw-r--r--src/templates/assets/javascripts/components/announce/index.ts110
-rw-r--r--src/templates/assets/javascripts/components/consent/index.ts116
-rw-r--r--src/templates/assets/javascripts/components/content/_/index.ts125
-rw-r--r--src/templates/assets/javascripts/components/content/annotation/_/index.ts272
-rw-r--r--src/templates/assets/javascripts/components/content/annotation/block/index.ts88
-rw-r--r--src/templates/assets/javascripts/components/content/annotation/index.ts25
-rw-r--r--src/templates/assets/javascripts/components/content/annotation/list/index.ts209
-rw-r--r--src/templates/assets/javascripts/components/content/code/_/index.ts238
-rw-r--r--src/templates/assets/javascripts/components/content/code/index.ts23
-rw-r--r--src/templates/assets/javascripts/components/content/details/index.ts138
-rw-r--r--src/templates/assets/javascripts/components/content/index.ts28
-rw-r--r--src/templates/assets/javascripts/components/content/mermaid/index.css430
-rw-r--r--src/templates/assets/javascripts/components/content/mermaid/index.ts133
-rw-r--r--src/templates/assets/javascripts/components/content/table/index.ts70
-rw-r--r--src/templates/assets/javascripts/components/content/tabs/index.ts265
-rw-r--r--src/templates/assets/javascripts/components/dialog/index.ts128
-rw-r--r--src/templates/assets/javascripts/components/header/_/index.ts200
-rw-r--r--src/templates/assets/javascripts/components/header/index.ts24
-rw-r--r--src/templates/assets/javascripts/components/header/title/index.ts144
-rw-r--r--src/templates/assets/javascripts/components/index.ts37
-rw-r--r--src/templates/assets/javascripts/components/main/index.ts125
-rw-r--r--src/templates/assets/javascripts/components/palette/index.ts180
-rw-r--r--src/templates/assets/javascripts/components/progress/index.ts87
-rw-r--r--src/templates/assets/javascripts/components/search/_/index.ts239
-rw-r--r--src/templates/assets/javascripts/components/search/highlight/.eslintrc5
-rw-r--r--src/templates/assets/javascripts/components/search/highlight/index.ts115
-rw-r--r--src/templates/assets/javascripts/components/search/index.ts28
-rw-r--r--src/templates/assets/javascripts/components/search/query/index.ts206
-rw-r--r--src/templates/assets/javascripts/components/search/result/index.ts197
-rw-r--r--src/templates/assets/javascripts/components/search/share/index.ts135
-rw-r--r--src/templates/assets/javascripts/components/search/suggest/index.ts154
-rw-r--r--src/templates/assets/javascripts/components/sidebar/index.ts227
-rw-r--r--src/templates/assets/javascripts/components/source/_/index.ts142
-rw-r--r--src/templates/assets/javascripts/components/source/facts/_/index.ts88
-rw-r--r--src/templates/assets/javascripts/components/source/facts/github/index.ts103
-rw-r--r--src/templates/assets/javascripts/components/source/facts/gitlab/index.ts61
-rw-r--r--src/templates/assets/javascripts/components/source/facts/index.ts25
-rw-r--r--src/templates/assets/javascripts/components/source/index.ts24
-rw-r--r--src/templates/assets/javascripts/components/tabs/index.ts144
-rw-r--r--src/templates/assets/javascripts/components/toc/index.ts379
-rw-r--r--src/templates/assets/javascripts/components/top/index.ts184
-rw-r--r--src/templates/assets/javascripts/integrations/clipboard/index.ts99
-rw-r--r--src/templates/assets/javascripts/integrations/index.ts27
-rw-r--r--src/templates/assets/javascripts/integrations/instant/.eslintrc6
-rw-r--r--src/templates/assets/javascripts/integrations/instant/index.ts446
-rw-r--r--src/templates/assets/javascripts/integrations/search/_/index.ts332
-rw-r--r--src/templates/assets/javascripts/integrations/search/config/index.ts115
-rw-r--r--src/templates/assets/javascripts/integrations/search/highlighter/index.ts93
-rw-r--r--src/templates/assets/javascripts/integrations/search/index.ts27
-rw-r--r--src/templates/assets/javascripts/integrations/search/internal/.eslintrc6
-rw-r--r--src/templates/assets/javascripts/integrations/search/internal/_/index.ts74
-rw-r--r--src/templates/assets/javascripts/integrations/search/internal/extract/index.ts107
-rw-r--r--src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts162
-rw-r--r--src/templates/assets/javascripts/integrations/search/internal/index.ts26
-rw-r--r--src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts136
-rw-r--r--src/templates/assets/javascripts/integrations/search/query/.eslintrc6
-rw-r--r--src/templates/assets/javascripts/integrations/search/query/_/index.ts172
-rw-r--r--src/templates/assets/javascripts/integrations/search/query/index.ts25
-rw-r--r--src/templates/assets/javascripts/integrations/search/query/segment/index.ts81
-rw-r--r--src/templates/assets/javascripts/integrations/search/query/transform/index.ts99
-rw-r--r--src/templates/assets/javascripts/integrations/search/worker/_/index.ts95
-rw-r--r--src/templates/assets/javascripts/integrations/search/worker/index.ts24
-rw-r--r--src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc6
-rw-r--r--src/templates/assets/javascripts/integrations/search/worker/main/index.ts192
-rw-r--r--src/templates/assets/javascripts/integrations/search/worker/message/index.ts112
-rw-r--r--src/templates/assets/javascripts/integrations/sitemap/index.ts107
-rw-r--r--src/templates/assets/javascripts/integrations/version/.eslintrc5
-rw-r--r--src/templates/assets/javascripts/integrations/version/index.ts186
-rw-r--r--src/templates/assets/javascripts/patches/indeterminate/index.ts85
-rw-r--r--src/templates/assets/javascripts/patches/index.ts25
-rw-r--r--src/templates/assets/javascripts/patches/scrollfix/index.ts100
-rw-r--r--src/templates/assets/javascripts/patches/scrolllock/index.ts89
-rw-r--r--src/templates/assets/javascripts/polyfills/index.ts96
-rw-r--r--src/templates/assets/javascripts/templates/annotation/index.tsx65
-rw-r--r--src/templates/assets/javascripts/templates/clipboard/index.tsx45
-rw-r--r--src/templates/assets/javascripts/templates/index.ts29
-rw-r--r--src/templates/assets/javascripts/templates/search/index.tsx170
-rw-r--r--src/templates/assets/javascripts/templates/source/index.tsx47
-rw-r--r--src/templates/assets/javascripts/templates/tabbed/index.tsx56
-rw-r--r--src/templates/assets/javascripts/templates/table/index.tsx44
-rw-r--r--src/templates/assets/javascripts/templates/tooltip/index.tsx42
-rw-r--r--src/templates/assets/javascripts/templates/version/index.tsx92
-rw-r--r--src/templates/assets/javascripts/utilities/h/.eslintrc7
-rw-r--r--src/templates/assets/javascripts/utilities/h/index.ts132
-rw-r--r--src/templates/assets/javascripts/utilities/index.ts24
-rw-r--r--src/templates/assets/javascripts/utilities/round/index.ts50
-rw-r--r--src/templates/assets/javascripts/workers/search.ts23
-rw-r--r--src/templates/assets/stylesheets/_config.scss42
-rw-r--r--src/templates/assets/stylesheets/main.scss86
-rw-r--r--src/templates/assets/stylesheets/main/_colors.scss153
-rw-r--r--src/templates/assets/stylesheets/main/_icons.scss37
-rw-r--r--src/templates/assets/stylesheets/main/_modifiers.scss48
-rw-r--r--src/templates/assets/stylesheets/main/_resets.scss118
-rw-r--r--src/templates/assets/stylesheets/main/_typeset.scss603
-rw-r--r--src/templates/assets/stylesheets/main/components/_author.scss86
-rw-r--r--src/templates/assets/stylesheets/main/components/_banner.scss68
-rw-r--r--src/templates/assets/stylesheets/main/components/_base.scss182
-rw-r--r--src/templates/assets/stylesheets/main/components/_clipboard.scss102
-rw-r--r--src/templates/assets/stylesheets/main/components/_consent.scss127
-rw-r--r--src/templates/assets/stylesheets/main/components/_content.scss97
-rw-r--r--src/templates/assets/stylesheets/main/components/_dialog.scss65
-rw-r--r--src/templates/assets/stylesheets/main/components/_feedback.scss110
-rw-r--r--src/templates/assets/stylesheets/main/components/_footer.scss201
-rw-r--r--src/templates/assets/stylesheets/main/components/_form.scss83
-rw-r--r--src/templates/assets/stylesheets/main/components/_header.scss270
-rw-r--r--src/templates/assets/stylesheets/main/components/_meta.scss67
-rw-r--r--src/templates/assets/stylesheets/main/components/_nav.scss761
-rw-r--r--src/templates/assets/stylesheets/main/components/_pagination.scss85
-rw-r--r--src/templates/assets/stylesheets/main/components/_post.scss196
-rw-r--r--src/templates/assets/stylesheets/main/components/_progress.scss53
-rw-r--r--src/templates/assets/stylesheets/main/components/_search.scss707
-rw-r--r--src/templates/assets/stylesheets/main/components/_select.scss115
-rw-r--r--src/templates/assets/stylesheets/main/components/_sidebar.scss209
-rw-r--r--src/templates/assets/stylesheets/main/components/_source.scss182
-rw-r--r--src/templates/assets/stylesheets/main/components/_status.scss73
-rw-r--r--src/templates/assets/stylesheets/main/components/_tabs.scss133
-rw-r--r--src/templates/assets/stylesheets/main/components/_tag.scss105
-rw-r--r--src/templates/assets/stylesheets/main/components/_tooltip.scss292
-rw-r--r--src/templates/assets/stylesheets/main/components/_top.scss83
-rw-r--r--src/templates/assets/stylesheets/main/components/_version.scss150
-rw-r--r--src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss195
-rw-r--r--src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss146
-rw-r--r--src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss92
-rw-r--r--src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss52
-rw-r--r--src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss76
-rw-r--r--src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss121
-rw-r--r--src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss43
-rw-r--r--src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss382
-rw-r--r--src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss115
-rw-r--r--src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss400
-rw-r--r--src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss78
-rw-r--r--src/templates/assets/stylesheets/main/integrations/_mermaid.scss67
-rw-r--r--src/templates/assets/stylesheets/palette.scss40
-rw-r--r--src/templates/assets/stylesheets/palette/_accent.scss61
-rw-r--r--src/templates/assets/stylesheets/palette/_primary.scss203
-rw-r--r--src/templates/assets/stylesheets/palette/_scheme.scss145
-rw-r--r--src/templates/assets/stylesheets/utilities/_break.scss219
-rw-r--r--src/templates/assets/stylesheets/utilities/_convert.scss79
-rw-r--r--src/templates/base.html445
-rw-r--r--src/templates/blog-post.html164
-rw-r--r--src/templates/blog.html48
-rw-r--r--src/templates/main.html23
-rw-r--r--src/templates/mkdocs_theme.yml50
-rw-r--r--src/templates/partials/actions.html54
-rw-r--r--src/templates/partials/alternate.html49
-rw-r--r--src/templates/partials/comments.html23
-rw-r--r--src/templates/partials/consent.html107
-rw-r--r--src/templates/partials/content.html54
-rw-r--r--src/templates/partials/copyright.html39
-rw-r--r--src/templates/partials/feedback.html79
-rw-r--r--src/templates/partials/footer.html98
-rw-r--r--src/templates/partials/header.html112
-rw-r--r--src/templates/partials/icons.html72
-rw-r--r--src/templates/partials/integrations/analytics.html49
-rw-r--r--src/templates/partials/integrations/analytics/google.html97
-rw-r--r--src/templates/partials/javascripts/announce.html31
-rw-r--r--src/templates/partials/javascripts/base.html48
-rw-r--r--src/templates/partials/javascripts/consent.html61
-rw-r--r--src/templates/partials/javascripts/content.html39
-rw-r--r--src/templates/partials/javascripts/outdated.html29
-rw-r--r--src/templates/partials/javascripts/palette.html29
-rw-r--r--src/templates/partials/language.html28
-rw-r--r--src/templates/partials/languages/af.html76
-rw-r--r--src/templates/partials/languages/ar.html77
-rw-r--r--src/templates/partials/languages/be.html77
-rw-r--r--src/templates/partials/languages/bg.html76
-rw-r--r--src/templates/partials/languages/bn.html76
-rw-r--r--src/templates/partials/languages/ca.html75
-rw-r--r--src/templates/partials/languages/cs.html75
-rw-r--r--src/templates/partials/languages/da.html76
-rw-r--r--src/templates/partials/languages/de.html76
-rw-r--r--src/templates/partials/languages/el.html74
-rw-r--r--src/templates/partials/languages/en.html79
-rw-r--r--src/templates/partials/languages/eo.html49
-rw-r--r--src/templates/partials/languages/es.html76
-rw-r--r--src/templates/partials/languages/et.html43
-rw-r--r--src/templates/partials/languages/eu.html75
-rw-r--r--src/templates/partials/languages/fa.html77
-rw-r--r--src/templates/partials/languages/fi.html44
-rw-r--r--src/templates/partials/languages/fr.html76
-rw-r--r--src/templates/partials/languages/gl.html56
-rw-r--r--src/templates/partials/languages/he.html77
-rw-r--r--src/templates/partials/languages/hi.html76
-rw-r--r--src/templates/partials/languages/hr.html75
-rw-r--r--src/templates/partials/languages/hu.html76
-rw-r--r--src/templates/partials/languages/hy.html76
-rw-r--r--src/templates/partials/languages/id.html76
-rw-r--r--src/templates/partials/languages/is.html75
-rw-r--r--src/templates/partials/languages/it.html76
-rw-r--r--src/templates/partials/languages/ja.html78
-rw-r--r--src/templates/partials/languages/ka.html49
-rw-r--r--src/templates/partials/languages/kn.html75
-rw-r--r--src/templates/partials/languages/ko.html76
-rw-r--r--src/templates/partials/languages/ku-IQ.html64
-rw-r--r--src/templates/partials/languages/lb.html76
-rw-r--r--src/templates/partials/languages/lt.html76
-rw-r--r--src/templates/partials/languages/lv.html55
-rw-r--r--src/templates/partials/languages/mk.html56
-rw-r--r--src/templates/partials/languages/mn.html51
-rw-r--r--src/templates/partials/languages/ms.html55
-rw-r--r--src/templates/partials/languages/my.html49
-rw-r--r--src/templates/partials/languages/nb.html76
-rw-r--r--src/templates/partials/languages/nl.html76
-rw-r--r--src/templates/partials/languages/nn.html62
-rw-r--r--src/templates/partials/languages/pl.html76
-rw-r--r--src/templates/partials/languages/pt-BR.html76
-rw-r--r--src/templates/partials/languages/pt.html76
-rw-r--r--src/templates/partials/languages/ro.html76
-rw-r--r--src/templates/partials/languages/ru.html76
-rw-r--r--src/templates/partials/languages/sa.html75
-rw-r--r--src/templates/partials/languages/sh.html70
-rw-r--r--src/templates/partials/languages/si.html51
-rw-r--r--src/templates/partials/languages/sk.html43
-rw-r--r--src/templates/partials/languages/sl.html76
-rw-r--r--src/templates/partials/languages/sr.html57
-rw-r--r--src/templates/partials/languages/sv.html76
-rw-r--r--src/templates/partials/languages/te.html75
-rw-r--r--src/templates/partials/languages/th.html76
-rw-r--r--src/templates/partials/languages/tl.html57
-rw-r--r--src/templates/partials/languages/tr.html76
-rw-r--r--src/templates/partials/languages/uk.html75
-rw-r--r--src/templates/partials/languages/ur.html77
-rw-r--r--src/templates/partials/languages/uz.html76
-rw-r--r--src/templates/partials/languages/vi.html76
-rw-r--r--src/templates/partials/languages/zh-Hant.html77
-rw-r--r--src/templates/partials/languages/zh-TW.html77
-rw-r--r--src/templates/partials/languages/zh.html77
-rw-r--r--src/templates/partials/logo.html29
-rw-r--r--src/templates/partials/nav-item.html249
-rw-r--r--src/templates/partials/nav.html69
-rw-r--r--src/templates/partials/pagination.html42
-rw-r--r--src/templates/partials/palette.html55
-rw-r--r--src/templates/partials/post.html99
-rw-r--r--src/templates/partials/progress.html24
-rw-r--r--src/templates/partials/search.html109
-rw-r--r--src/templates/partials/social.html48
-rw-r--r--src/templates/partials/source-file.html44
-rw-r--r--src/templates/partials/source.html37
-rw-r--r--src/templates/partials/tabs-item.html71
-rw-r--r--src/templates/partials/tabs.html38
-rw-r--r--src/templates/partials/tags.html52
-rw-r--r--src/templates/partials/toc-item.html39
-rw-r--r--src/templates/partials/toc.html56
-rw-r--r--src/templates/partials/top.html28
-rw-r--r--src/templates/redirect.html41
337 files changed, 34524 insertions, 0 deletions
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 00000000..f875ee63
--- /dev/null
+++ b/src/__init__.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+__version__ = "$md-version$"
diff --git a/src/extensions/__init__.py b/src/extensions/__init__.py
new file mode 100644
index 00000000..d1899378
--- /dev/null
+++ b/src/extensions/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
diff --git a/src/extensions/emoji.py b/src/extensions/emoji.py
new file mode 100644
index 00000000..c8c955cc
--- /dev/null
+++ b/src/extensions/emoji.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from __future__ import annotations
+
+import codecs
+import functools
+import material
+import os
+
+from glob import iglob
+from inspect import getfile
+from markdown import Markdown
+from pymdownx import emoji, twemoji_db
+from xml.etree.ElementTree import Element
+
+# -----------------------------------------------------------------------------
+# Functions
+# -----------------------------------------------------------------------------
+
+# Create twemoji index
+def twemoji(options: object, md: Markdown):
+ paths = options.get("custom_icons", [])[:]
+ return _load_twemoji_index(tuple(paths))
+
+# Create emoji or icon
+def to_svg(
+ index: str, shortname: str, alias: str, uc: str | None, alt: str,
+ title: str, category: str, options: object, md: Markdown
+):
+ if not uc:
+ icons = md.inlinePatterns["emoji"].emoji_index["emoji"]
+
+ # Create and return element to host icon
+ el = Element("span", { "class": options.get("classes", index) })
+ el.text = md.htmlStash.store(_load(icons[shortname]["path"]))
+ return el
+
+ # Delegate to `pymdownx.emoji` extension
+ return emoji.to_svg(
+ index, shortname, alias, uc, alt, title, category, options, md
+ )
+
+# -----------------------------------------------------------------------------
+# Helper functions
+# -----------------------------------------------------------------------------
+
+# Load icon
+@functools.lru_cache(maxsize = None)
+def _load(file: str):
+ with codecs.open(file, encoding = "utf-8") as f:
+ return f.read()
+
+# Load twemoji index and add icons
+@functools.lru_cache(maxsize = None)
+def _load_twemoji_index(paths):
+ index = {
+ "name": "twemoji",
+ "emoji": twemoji_db.emoji,
+ "aliases": twemoji_db.aliases
+ }
+
+ # Compute path to theme root and traverse all icon directories
+ root = os.path.dirname(getfile(material))
+ root = os.path.join(root, "templates", ".icons")
+ for path in [*paths, root]:
+ base = os.path.normpath(path)
+
+ # Index icons provided by the theme and via custom icons
+ glob = os.path.join(base, "**", "*.svg")
+ glob = iglob(os.path.normpath(glob), recursive = True)
+ for file in glob:
+ icon = file[len(base) + 1:-4].replace(os.path.sep, "-")
+
+ # Add icon to index
+ name = f":{icon}:"
+ if not any(name in index[key] for key in ["emoji", "aliases"]):
+ index["emoji"][name] = { "name": name, "path": file }
+
+ # Return index
+ return index
diff --git a/src/overrides/assets/javascripts/components/_/index.ts b/src/overrides/assets/javascripts/components/_/index.ts
new file mode 100644
index 00000000..3cb4c18e
--- /dev/null
+++ b/src/overrides/assets/javascripts/components/_/index.ts
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { getElement, getElements } from "~/browser"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Component type
+ */
+export type ComponentType =
+ | "iconsearch" /* Icon search */
+ | "iconsearch-query" /* Icon search input */
+ | "iconsearch-result" /* Icon search results */
+ | "sponsorship" /* Sponsorship */
+ | "sponsorship-count" /* Sponsorship count */
+ | "sponsorship-total" /* Sponsorship total */
+
+/**
+ * Component
+ *
+ * @template T - Component type
+ * @template U - Reference type
+ */
+export type Component<
+ T extends {} = {},
+ U extends HTMLElement = HTMLElement
+> =
+ T & {
+ ref: U /* Component reference */
+ }
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Component type map
+ */
+interface ComponentTypeMap {
+ "iconsearch": HTMLElement /* Icon search */
+ "iconsearch-query": HTMLInputElement /* Icon search input */
+ "iconsearch-result": HTMLElement /* Icon search results */
+ "sponsorship": HTMLElement /* Sponsorship */
+ "sponsorship-count": HTMLElement /* Sponsorship count */
+ "sponsorship-total": HTMLElement /* Sponsorship total */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve the element for a given component or throw a reference error
+ *
+ * @template T - Component type
+ *
+ * @param type - Component type
+ * @param node - Node of reference
+ *
+ * @returns Element
+ */
+export function getComponentElement<T extends ComponentType>(
+ type: T, node: ParentNode = document
+): ComponentTypeMap[T] {
+ return getElement(`[data-mdx-component=${type}]`, node)
+}
+
+/**
+ * Retrieve all elements for a given component
+ *
+ * @template T - Component type
+ *
+ * @param type - Component type
+ * @param node - Node of reference
+ *
+ * @returns Elements
+ */
+export function getComponentElements<T extends ComponentType>(
+ type: T, node: ParentNode = document
+): ComponentTypeMap[T][] {
+ return getElements(`[data-mdx-component=${type}]`, node)
+}
diff --git a/src/overrides/assets/javascripts/components/iconsearch/_/index.ts b/src/overrides/assets/javascripts/components/iconsearch/_/index.ts
new file mode 100644
index 00000000..f509a6f9
--- /dev/null
+++ b/src/overrides/assets/javascripts/components/iconsearch/_/index.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { Observable, merge } from "rxjs"
+
+import { configuration } from "~/_"
+import { requestJSON } from "~/browser"
+
+import { Component, getComponentElement } from "../../_"
+import {
+ IconSearchQuery,
+ mountIconSearchQuery
+} from "../query"
+import {
+ IconSearchResult,
+ mountIconSearchResult
+} from "../result"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Icon category
+ */
+export interface IconCategory {
+ base: string /* Category base URL */
+ data: Record<string, string> /* Category data */
+}
+
+/**
+ * Icon search index
+ */
+export interface IconSearchIndex {
+ icons: IconCategory /* Icons */
+ emojis: IconCategory /* Emojis */
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Icon search
+ */
+export type IconSearch =
+ | IconSearchQuery
+ | IconSearchResult
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount icon search
+ *
+ * @param el - Icon search element
+ *
+ * @returns Icon search component observable
+ */
+export function mountIconSearch(
+ el: HTMLElement
+): Observable<Component<IconSearch>> {
+ const config = configuration()
+ const index$ = requestJSON<IconSearchIndex>(
+ new URL("assets/javascripts/iconsearch_index.json", config.base)
+ )
+
+ /* Retrieve query and result components */
+ const query = getComponentElement("iconsearch-query", el)
+ const result = getComponentElement("iconsearch-result", el)
+
+ /* Create and return component */
+ const query$ = mountIconSearchQuery(query)
+ const result$ = mountIconSearchResult(result, { index$, query$ })
+ return merge(query$, result$)
+}
diff --git a/src/overrides/assets/javascripts/components/iconsearch/index.ts b/src/overrides/assets/javascripts/components/iconsearch/index.ts
new file mode 100644
index 00000000..9d856774
--- /dev/null
+++ b/src/overrides/assets/javascripts/components/iconsearch/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./query"
+export * from "./result"
diff --git a/src/overrides/assets/javascripts/components/iconsearch/query/index.ts b/src/overrides/assets/javascripts/components/iconsearch/query/index.ts
new file mode 100644
index 00000000..03a3daad
--- /dev/null
+++ b/src/overrides/assets/javascripts/components/iconsearch/query/index.ts
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ combineLatest,
+ delay,
+ distinctUntilChanged,
+ filter,
+ fromEvent,
+ map,
+ merge,
+ startWith,
+ withLatestFrom
+} from "rxjs"
+
+import { watchElementFocus } from "~/browser"
+
+import { Component } from "../../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Icon search query
+ */
+export interface IconSearchQuery {
+ value: string /* Query value */
+ focus: boolean /* Query focus */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount icon search query
+ *
+ * @param el - Icon search query element
+ *
+ * @returns Icon search query component observable
+ */
+export function mountIconSearchQuery(
+ el: HTMLInputElement
+): Observable<Component<IconSearchQuery, HTMLInputElement>> {
+
+ /* Intercept focus and input events */
+ const focus$ = watchElementFocus(el)
+ const value$ = merge(
+ fromEvent(el, "keyup"),
+ fromEvent(el, "focus").pipe(delay(1))
+ )
+ .pipe(
+ map(() => el.value),
+ startWith(el.value),
+ distinctUntilChanged(),
+ )
+
+ /* Log search on blur */
+ focus$
+ .pipe(
+ filter(active => !active),
+ withLatestFrom(value$)
+ )
+ .subscribe(([, value]) => {
+ const path = document.location.pathname
+ if (typeof ga === "function" && value.length)
+ ga("send", "pageview", `${path}?q=[icon]+${value}`)
+ })
+
+ /* Combine into single observable */
+ return combineLatest([value$, focus$])
+ .pipe(
+ map(([value, focus]) => ({ ref: el, value, focus })),
+ )
+}
diff --git a/src/overrides/assets/javascripts/components/iconsearch/result/index.ts b/src/overrides/assets/javascripts/components/iconsearch/result/index.ts
new file mode 100644
index 00000000..2b9d97fb
--- /dev/null
+++ b/src/overrides/assets/javascripts/components/iconsearch/result/index.ts
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { filter as search } from "fuzzaldrin-plus"
+import {
+ Observable,
+ Subject,
+ bufferCount,
+ combineLatest,
+ distinctUntilKeyChanged,
+ filter,
+ finalize,
+ map,
+ merge,
+ of,
+ switchMap,
+ tap,
+ withLatestFrom,
+ zipWith
+} from "rxjs"
+
+import {
+ getElement,
+ watchElementBoundary
+} from "~/browser"
+import { round } from "~/utilities"
+
+import { Icon, renderIconSearchResult } from "_/templates"
+
+import { Component } from "../../_"
+import { IconSearchIndex } from "../_"
+import { IconSearchQuery } from "../query"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Icon search result
+ */
+export interface IconSearchResult {
+ data: Icon[] /* Search result data */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ index$: Observable<IconSearchIndex> /* Search index observable */
+ query$: Observable<IconSearchQuery> /* Search query observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ index$: Observable<IconSearchIndex> /* Search index observable */
+ query$: Observable<IconSearchQuery> /* Search query observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch icon search result
+ *
+ * @param el - Icon search result element
+ * @param options - Options
+ *
+ * @returns Icon search result observable
+ */
+export function watchIconSearchResult(
+ el: HTMLElement, { index$, query$ }: WatchOptions
+): Observable<IconSearchResult> {
+ switch (el.getAttribute("data-mdx-mode")) {
+
+ case "file":
+ return combineLatest([
+ query$.pipe(distinctUntilKeyChanged("value")),
+ index$
+ .pipe(
+ map(({ icons }) => Object.values(icons.data)
+ .map(icon => icon.replace(/\.svg$/, ""))
+ )
+ )
+ ])
+ .pipe(
+ map(([{ value }, data]) => search(data, value)),
+ switchMap(files => index$.pipe(
+ map(({ icons }) => ({
+ data: files.map<Icon>(shortcode => {
+ return {
+ shortcode,
+ url: [
+ icons.base,
+ shortcode,
+ ".svg"
+ ].join("")
+ }
+ })
+ }))
+ ))
+ )
+
+ default:
+ return combineLatest([
+ query$.pipe(distinctUntilKeyChanged("value")),
+ index$
+ .pipe(
+ map(({ icons, emojis }) => [
+ ...Object.keys(icons.data),
+ ...Object.keys(emojis.data)
+ ])
+ )
+ ])
+ .pipe(
+ map(([{ value }, data]) => search(data, value)),
+ switchMap(shortcodes => index$.pipe(
+ map(({ icons, emojis }) => ({
+ data: shortcodes.map<Icon>(shortcode => {
+ const category =
+ shortcode in icons.data
+ ? icons
+ : emojis
+ return {
+ shortcode,
+ url: [
+ category.base,
+ category.data[shortcode]
+ ].join("")
+ }
+ })
+ }))
+ ))
+ )
+ }
+}
+
+/**
+ * Mount icon search result
+ *
+ * @param el - Icon search result element
+ * @param options - Options
+ *
+ * @returns Icon search result component observable
+ */
+export function mountIconSearchResult(
+ el: HTMLElement, { index$, query$ }: MountOptions
+): Observable<Component<IconSearchResult, HTMLElement>> {
+ const push$ = new Subject<IconSearchResult>()
+ const boundary$ = watchElementBoundary(el)
+ .pipe(
+ filter(Boolean)
+ )
+
+ /* Update search result metadata */
+ const meta = getElement(":scope > :first-child", el)
+ push$
+ .pipe(
+ withLatestFrom(query$)
+ )
+ .subscribe(([{ data }, { value }]) => {
+ if (value) {
+ switch (data.length) {
+
+ /* No results */
+ case 0:
+ meta.textContent = "No matches"
+ break
+
+ /* One result */
+ case 1:
+ meta.textContent = "1 match"
+ break
+
+ /* Multiple result */
+ default:
+ meta.textContent = `${round(data.length)} matches`
+ }
+ } else {
+ meta.textContent = "Type to start searching"
+ }
+ })
+
+ /* Update icon search result list */
+ const file = el.getAttribute("data-mdx-mode") === "file"
+ const list = getElement(":scope > :last-child", el)
+ push$
+ .pipe(
+ tap(() => list.innerHTML = ""),
+ switchMap(({ data }) => merge(
+ of(...data.slice(0, 10)),
+ of(...data.slice(10))
+ .pipe(
+ bufferCount(10),
+ zipWith(boundary$),
+ switchMap(([chunk]) => chunk)
+ )
+ )),
+ withLatestFrom(query$)
+ )
+ .subscribe(([result, { value }]) => list.appendChild(
+ renderIconSearchResult(result, value, file)
+ ))
+
+ /* Create and return component */
+ return watchIconSearchResult(el, { query$, index$ })
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+}
diff --git a/src/overrides/assets/javascripts/components/index.ts b/src/overrides/assets/javascripts/components/index.ts
new file mode 100644
index 00000000..ec6c9dce
--- /dev/null
+++ b/src/overrides/assets/javascripts/components/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./iconsearch"
+export * from "./sponsorship"
diff --git a/src/overrides/assets/javascripts/components/sponsorship/index.ts b/src/overrides/assets/javascripts/components/sponsorship/index.ts
new file mode 100644
index 00000000..711f423a
--- /dev/null
+++ b/src/overrides/assets/javascripts/components/sponsorship/index.ts
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { Observable, map } from "rxjs"
+
+import { getElement, requestJSON } from "~/browser"
+
+import { renderPrivateSponsor, renderPublicSponsor } from "_/templates"
+
+import { Component, getComponentElement } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Sponsor type
+ */
+export type SponsorType =
+ | "user" /* Sponsor is a user */
+ | "organization" /* Sponsor is an organization */
+
+/**
+ * Sponsor visibility
+ */
+export type SponsorVisibility =
+ | "public" /* Sponsor is a user */
+ | "private" /* Sponsor is an organization */
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Sponsor user
+ */
+export interface SponsorUser {
+ type: SponsorType /* Sponsor type */
+ name: string /* Sponsor login name */
+ image: string /* Sponsor image URL */
+ url: string /* Sponsor URL */
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Public sponsor
+ */
+export interface PublicSponsor {
+ type: "public" /* Sponsor visibility */
+ user: SponsorUser /* Sponsor user */
+}
+
+/**
+ * Private sponsor
+ */
+export interface PrivateSponsor {
+ type: "private" /* Sponsor visibility */
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Sponsor
+ */
+export type Sponsor =
+ | PublicSponsor
+ | PrivateSponsor
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Sponsorship
+ */
+export interface Sponsorship {
+ sponsors: Sponsor[] /* Sponsors */
+ total: number /* Total amount */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount sponsorship
+ *
+ * @param el - Sponsorship element
+ *
+ * @returns Sponsorship component observable
+ */
+export function mountSponsorship(
+ el: HTMLElement
+): Observable<Component<Sponsorship>> {
+ const sponsorship$ = requestJSON<Sponsorship>(
+ "https://3if8u9o552.execute-api.us-east-1.amazonaws.com/_/"
+ )
+
+ /* Retrieve adjacent components */
+ const count = getComponentElement("sponsorship-count")
+ const total = getComponentElement("sponsorship-total")
+
+ /* Render sponsorship */
+ sponsorship$.subscribe(sponsorship => {
+ el.removeAttribute("hidden")
+
+ /* Render public sponsors with avatar and links */
+ const list = getElement(":scope > :first-child", el)
+ for (const sponsor of sponsorship.sponsors)
+ if (sponsor.type === "public")
+ list.appendChild(renderPublicSponsor(sponsor.user))
+
+ /* Render combined private sponsors */
+ list.appendChild(renderPrivateSponsor(
+ sponsorship.sponsors.filter(({ type }) => (
+ type === "private"
+ )).length
+ ))
+
+ /* Render sponsorship count and total */
+ count.innerText = `${sponsorship.sponsors.length}`
+ total.innerText = `$ ${sponsorship.total
+ .toString()
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ",")
+ } a month`
+ })
+
+ // /* Create and return component */
+ return sponsorship$
+ .pipe(
+ map(state => ({ ref: el, ...state }))
+ )
+}
diff --git a/src/overrides/assets/javascripts/custom.ts b/src/overrides/assets/javascripts/custom.ts
new file mode 100644
index 00000000..7c3c3847
--- /dev/null
+++ b/src/overrides/assets/javascripts/custom.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { merge, switchMap } from "rxjs"
+
+import {
+ getComponentElements,
+ mountIconSearch,
+ mountSponsorship
+} from "./components"
+import { setupAnalytics } from "./integrations"
+
+/* ----------------------------------------------------------------------------
+ * Application
+ * ------------------------------------------------------------------------- */
+
+/* Set up extra analytics events */
+setupAnalytics()
+
+/* Set up extra component observables */
+const component$ = document$
+ .pipe(
+ switchMap(() => merge(
+
+ /* Icon search */
+ ...getComponentElements("iconsearch")
+ .map(el => mountIconSearch(el)),
+
+ /* Sponsorship */
+ ...getComponentElements("sponsorship")
+ .map(el => mountSponsorship(el))
+ ))
+ )
+
+/* Subscribe to all components */
+component$.subscribe()
diff --git a/src/overrides/assets/javascripts/integrations/analytics/index.ts b/src/overrides/assets/javascripts/integrations/analytics/index.ts
new file mode 100644
index 00000000..658add2a
--- /dev/null
+++ b/src/overrides/assets/javascripts/integrations/analytics/index.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { fromEvent } from "rxjs"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Set up extra analytics events
+ */
+export function setupAnalytics(): void {
+ const { origin } = new URL(location.href)
+ fromEvent(document.body, "click")
+ .subscribe(ev => {
+ if (ev.target instanceof HTMLElement) {
+ const el = ev.target.closest("a")
+ if (el && el.origin !== origin)
+ ga("send", "event", "outbound", "click", el.href)
+ }
+ })
+}
diff --git a/src/overrides/assets/javascripts/integrations/index.ts b/src/overrides/assets/javascripts/integrations/index.ts
new file mode 100644
index 00000000..9179f2a2
--- /dev/null
+++ b/src/overrides/assets/javascripts/integrations/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./analytics"
diff --git a/src/overrides/assets/javascripts/templates/iconsearch/index.tsx b/src/overrides/assets/javascripts/templates/iconsearch/index.tsx
new file mode 100644
index 00000000..13cafa6d
--- /dev/null
+++ b/src/overrides/assets/javascripts/templates/iconsearch/index.tsx
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { wrap } from "fuzzaldrin-plus"
+
+import { translation } from "~/_"
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Icon
+ */
+export interface Icon {
+ shortcode: string /* Icon shortcode */
+ url: string /* Icon URL */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Highlight an icon search result
+ *
+ * @param icon - Icon
+ * @param query - Search query
+ *
+ * @returns Highlighted result
+ */
+function highlight(icon: Icon, query: string): string {
+ return wrap(icon.shortcode, query, {
+ wrap: {
+ tagOpen: "<b>",
+ tagClose: "</b>"
+ }
+ })
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render an icon search result
+ *
+ * @param icon - Icon
+ * @param query - Search query
+ * @param file - Render as file
+ *
+ * @returns Element
+ */
+export function renderIconSearchResult(
+ icon: Icon, query: string, file?: boolean
+): HTMLElement {
+ return (
+ <li class="mdx-iconsearch-result__item">
+ <span class="twemoji">
+ <img src={icon.url} />
+ </span>
+ <button
+ class="md-clipboard--inline"
+ title={translation("clipboard.copy")}
+ data-clipboard-text={file ? icon.shortcode : `:${icon.shortcode}:`}
+ >
+ <code>{
+ file
+ ? highlight(icon, query)
+ : `:${highlight(icon, query)}:`
+ }</code>
+ </button>
+ </li>
+ )
+}
diff --git a/src/overrides/assets/javascripts/templates/index.ts b/src/overrides/assets/javascripts/templates/index.ts
new file mode 100644
index 00000000..02376b3d
--- /dev/null
+++ b/src/overrides/assets/javascripts/templates/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./iconsearch"
+export * from "./sponsorship"
diff --git a/src/overrides/assets/javascripts/templates/sponsorship/index.tsx b/src/overrides/assets/javascripts/templates/sponsorship/index.tsx
new file mode 100644
index 00000000..7891c2e0
--- /dev/null
+++ b/src/overrides/assets/javascripts/templates/sponsorship/index.tsx
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { h } from "~/utilities"
+
+import { SponsorUser } from "_/components"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render public sponsor
+ *
+ * @param user - Sponsor user
+ *
+ * @returns Element
+ */
+export function renderPublicSponsor(
+ user: SponsorUser
+): HTMLElement {
+ const title = `@${user.name}`
+ return (
+ <a href={user.url} title={title} class="mdx-sponsorship__item">
+ <img src={user.image} />
+ </a>
+ )
+}
+
+/**
+ * Render private sponsor
+ *
+ * @param count - Number of private sponsors
+ *
+ * @returns Element
+ */
+export function renderPrivateSponsor(
+ count: number
+): HTMLElement {
+ return (
+ <a
+ href="https://github.com/sponsors/squidfunk?metadata_origin=docs"
+ class="mdx-sponsorship__item mdx-sponsorship__item--private"
+ >
+ +{count}
+ </a>
+ )
+}
diff --git a/src/overrides/assets/stylesheets/custom.scss b/src/overrides/assets/stylesheets/custom.scss
new file mode 100644
index 00000000..8235e7d0
--- /dev/null
+++ b/src/overrides/assets/stylesheets/custom.scss
@@ -0,0 +1,44 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Dependencies
+// ----------------------------------------------------------------------------
+
+@import "material-color";
+@import "material-shadows";
+
+// ----------------------------------------------------------------------------
+// Local imports
+// ----------------------------------------------------------------------------
+
+@import "utilities/break";
+@import "utilities/convert";
+
+@import "config";
+
+@import "custom/typeset";
+
+@import "custom/layout/banner";
+@import "custom/layout/hero";
+@import "custom/layout/iconsearch";
+@import "custom/layout/sponsorship";
diff --git a/src/overrides/assets/stylesheets/custom/_typeset.scss b/src/overrides/assets/stylesheets/custom/_typeset.scss
new file mode 100644
index 00000000..bef30073
--- /dev/null
+++ b/src/overrides/assets/stylesheets/custom/_typeset.scss
@@ -0,0 +1,294 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Keyframes
+// ----------------------------------------------------------------------------
+
+// Pumping heart animation
+@keyframes heart {
+ 0%,
+ 40%,
+ 80%,
+ 100% {
+ transform: scale(1);
+ }
+
+ 20%,
+ 60% {
+ transform: scale(1.15);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Twitter icon
+ .twitter {
+ color: #00acee;
+ }
+
+ // Mastodon icon - it's not the exact brand color, because that doesn't work
+ // well on dark backgrounds, so we lightened it up a bit.
+ .mastodon {
+ color: #897ff8;
+ }
+
+ // Insiders video
+ .mdx-video {
+ width: auto;
+
+ // Insiders video container
+ &__inner {
+ position: relative;
+ width: 100%;
+ height: 0;
+ padding-bottom: 56.138%;
+ }
+
+ // Insiders video iframe
+ iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ border: none;
+ }
+ }
+
+ // Pumping heart
+ .mdx-heart {
+ animation: heart 1000ms infinite;
+ }
+
+ // Insiders color (for links, etc.) // remove
+ .mdx-insiders {
+ color: $clr-pink-500;
+ }
+
+ // BETA #####################################################################
+
+ // Badge
+ .mdx-badge {
+ font-size: 0.85em;
+
+ // Badge with heart
+ &--heart {
+ --md-typeset-a-color: hsla(#{hex2hsl($clr-pink-500)}, 1);
+ --md-accent-fg-color: hsla(#{hex2hsl($clr-pink-a200)}, 1);
+ --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-pink-500)}, 0.1);
+
+ // Animate icon
+ .twemoji {
+ animation: heart 1000ms infinite;
+ }
+ }
+
+ // Badge moved to the right
+ &--right {
+ float: right;
+ margin-left: 0.35em;
+ }
+
+ // Badge icon
+ &__icon {
+ padding: px2rem(4px);
+ background: var(--md-accent-fg-color--transparent);
+ border-start-start-radius: px2rem(2px);
+ border-end-start-radius: px2rem(2px);
+
+ // If icon is alone, round corners
+ &:last-child {
+ border-radius: px2rem(2px);
+ }
+ }
+
+ // Badge text
+ &__text {
+ padding: px2rem(4px) px2rem(6px);
+ border-start-end-radius: px2rem(2px);
+ border-end-end-radius: px2rem(2px);
+ box-shadow: 0 0 0 1px inset var(--md-accent-fg-color--transparent);
+ }
+ }
+
+ // BETA #####################################################################
+
+ // Switch buttons
+ .mdx-switch button {
+ cursor: pointer;
+ transition: opacity 250ms;
+
+ // Button on focus/hover
+ &:is(:focus, :hover) {
+ opacity: 0.75;
+ }
+
+ // Code block
+ > code {
+ display: block;
+ color: var(--md-primary-bg-color);
+ background-color: var(--md-primary-fg-color);
+ }
+ }
+
+ // Two-column layout
+ .mdx-columns {
+
+ // Column
+ ol,
+ ul {
+ columns: 2;
+
+ // [mobile portrait -]: Reset columns on mobile
+ @include break-to-device(mobile portrait) {
+ columns: initial;
+ }
+ }
+
+ // Column item
+ li {
+ break-inside: avoid;
+ }
+ }
+
+ // Language list
+ .mdx-flags {
+ margin: 2em auto;
+
+ // Language list
+ ol {
+ list-style: none;
+
+ // Language list item
+ li {
+ margin-bottom: 1em;
+ }
+ }
+
+ // Language item
+ &__item {
+ display: flex;
+ gap: px2rem(12px);
+ }
+
+ // Language content
+ &__content {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+
+ // Language name
+ span {
+ display: inline-flex;
+ align-items: baseline;
+ justify-content: space-between;
+ }
+
+ // Language link
+ > span:nth-child(2) {
+ font-size: 80%;
+ }
+
+ // Language code
+ code {
+ float: right;
+ }
+ }
+ }
+
+ // Social card
+ .mdx-social {
+ position: relative;
+ height: min(#{px2rem(540px)}, 80vw);
+
+ // Social card image on hover
+ &:hover .mdx-social__image {
+ background-color: rgba(228, 228, 228, 0.05);
+ }
+
+ // Social card layer
+ &__layer {
+ position: absolute;
+ margin-top: px2rem(80px);
+ transition: 250ms cubic-bezier(0.7, 0, 0.3, 1);
+ transform-style: preserve-3d;
+
+ // Social card layer on hover
+ &:hover {
+
+ // Social card label
+ .mdx-social__label {
+ opacity: 1;
+ }
+
+ // Social card image
+ .mdx-social__image {
+ background-color: rgba(127, 127, 127, 0.99);
+ }
+
+ // Hide top layers
+ ~ .mdx-social__layer {
+ opacity: 0;
+ }
+ }
+ }
+
+ // Social card image
+ &__image {
+ box-shadow:
+ px2rem(-5px) px2rem(5px) px2rem(10px)
+ rgba(0, 0, 0, 0.05);
+ transition: all 250ms;
+ transform: rotate(-40deg) skew(15deg, 15deg) scale(0.7);
+
+ // Actual image
+ img {
+ display: block;
+ }
+ }
+
+ // Social card label
+ &__label {
+ position: absolute;
+ display: block;
+ padding: px2rem(4px) px2rem(8px);
+ color: var(--md-default-bg-color);
+ background-color: var(--md-default-fg-color--light);
+ opacity: 0;
+ transition: all 250ms;
+ }
+
+ // Transform on hover
+ @for $i from 6 through 0 {
+ &:hover .mdx-social__layer:nth-child(#{$i}) {
+ transform: translateY(#{($i - 3) * -10}px);
+ }
+ }
+ }
+}
diff --git a/src/overrides/assets/stylesheets/custom/layout/_banner.scss b/src/overrides/assets/stylesheets/custom/layout/_banner.scss
new file mode 100644
index 00000000..b67d7fff
--- /dev/null
+++ b/src/overrides/assets/stylesheets/custom/layout/_banner.scss
@@ -0,0 +1,66 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Banner for announcements and warnings
+.md-banner {
+ color: var(--md-footer-fg-color--lighter);
+
+ // Don't wrap name of blog article
+ strong {
+ color: var(--md-footer-fg-color);
+ white-space: nowrap;
+ }
+
+ a {
+ color: var(--md-footer-fg-color);
+
+ &:focus,
+ &:hover {
+ color: currentcolor;
+
+ .twemoji {
+ background-color: var(--md-footer-fg-color);
+ box-shadow: none;
+ }
+ }
+ }
+
+ .twemoji {
+ display: inline-block;
+ width: px2rem(24px);
+ height: px2rem(24px);
+ padding: px2rem(5px);
+ vertical-align: bottom;
+ border-radius: 100%;
+ box-shadow: 0 0 0 px2rem(1px) currentcolor inset;
+ transition: all 250ms;
+
+ svg {
+ display: block;
+ max-height: initial;
+ }
+ }
+}
diff --git a/src/overrides/assets/stylesheets/custom/layout/_hero.scss b/src/overrides/assets/stylesheets/custom/layout/_hero.scss
new file mode 100644
index 00000000..428cd37e
--- /dev/null
+++ b/src/overrides/assets/stylesheets/custom/layout/_hero.scss
@@ -0,0 +1,123 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Landing page container
+.mdx-container {
+ padding-top: px2rem(20px);
+ background:
+ url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1123 258'><path d='M1124,2c0,0 0,256 0,256l-1125,0l0,-48c0,0 16,5 55,5c116,0 197,-92 325,-92c121,0 114,46 254,46c140,0 214,-167 572,-166Z' style='fill: hsla(0, 0%, 100%, 1)' /></svg>") no-repeat bottom,
+ linear-gradient(
+ to bottom,
+ var(--md-primary-fg-color),
+ hsla(280, 67%, 55%, 1) 99%,
+ var(--md-default-bg-color) 99%
+ );
+
+ // Adjust background for slate theme
+ [data-md-color-scheme="slate"] & {
+ background:
+ url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1123 258'><path d='M1124,2c0,0 0,256 0,256l-1125,0l0,-48c0,0 16,5 55,5c116,0 197,-92 325,-92c121,0 114,46 254,46c140,0 214,-167 572,-166Z' style='fill: hsla(230, 15%, 14%, 1)' /></svg>") no-repeat bottom,
+ linear-gradient(
+ to bottom,
+ var(--md-primary-fg-color),
+ hsla(230, 15%, 25%, 1) 99%,
+ var(--md-default-bg-color) 99%
+ );
+ }
+}
+
+// Landing page hero
+.mdx-hero {
+ margin: 0 px2rem(16px);
+ color: var(--md-primary-bg-color);
+
+ // Hero headline
+ h1 {
+ margin-bottom: px2rem(20px);
+ font-weight: 700;
+ color: currentcolor;
+
+ // [mobile portrait -]: Larger hero headline
+ @include break-to-device(mobile portrait) {
+ font-size: px2rem(28px);
+ }
+ }
+
+ // Hero content
+ &__content {
+ padding-bottom: px2rem(120px);
+ }
+
+ // [tablet landscape +]: Columnar display
+ @include break-from-device(tablet landscape) {
+ display: flex;
+ align-items: stretch;
+
+ // Adjust spacing and set dimensions
+ &__content {
+ max-width: px2rem(380px);
+ padding-bottom: 14vw;
+ margin-top: px2rem(70px);
+ }
+
+ // Hero image
+ &__image {
+ order: 1;
+ width: px2rem(760px);
+ transform: translateX(#{px2rem(80px)});
+ }
+ }
+
+ // [screen +]: Columnar display and adjusted spacing
+ @include break-from-device(screen) {
+
+ // Hero image
+ &__image {
+ transform: translateX(#{px2rem(160px)});
+ }
+ }
+
+ // Button
+ .md-button {
+ margin-top: px2rem(10px);
+ margin-right: px2rem(10px);
+ color: var(--md-primary-bg-color);
+
+ // Button on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-bg-color);
+ background-color: var(--md-accent-fg-color);
+ border-color: var(--md-accent-fg-color);
+ }
+
+ // Primary button
+ &--primary {
+ color: hsla(280, 37%, 48%, 1);
+ background-color: var(--md-primary-bg-color);
+ border-color: var(--md-primary-bg-color);
+ }
+ }
+}
diff --git a/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss b/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss
new file mode 100644
index 00000000..651c4135
--- /dev/null
+++ b/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss
@@ -0,0 +1,136 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Icon search
+ .mdx-iconsearch {
+ position: relative;
+ background-color: var(--md-default-bg-color);
+ border-radius: px2rem(2px);
+ box-shadow: var(--md-shadow-z1);
+ transition: box-shadow 125ms;
+
+ // Icon search on focus/hover
+ &:is(:focus-within, :hover) {
+ box-shadow: var(--md-shadow-z2);
+ }
+
+ // Icon search input
+ .md-input {
+ background: var(--md-default-bg-color);
+ box-shadow: none;
+
+ // Slate theme, i.e. dark mode
+ [data-md-color-scheme="slate"] & {
+ background: var(--md-code-bg-color);
+ }
+ }
+ }
+
+ // Icon search result
+ .mdx-iconsearch-result {
+ max-height: 50vh;
+ overflow-y: auto;
+ // Hack: promote to own layer to reduce jitter
+ backface-visibility: hidden;
+ touch-action: pan-y;
+ scrollbar-width: thin;
+ scrollbar-color: var(--md-default-fg-color--lighter) transparent;
+
+ // Icon search result inside tooltip
+ .md-tooltip & {
+ max-height: px2rem(205px);
+ }
+
+ // Webkit scrollbar
+ &::-webkit-scrollbar {
+ width: px2rem(4px);
+ height: px2rem(4px);
+ }
+
+ // Webkit scrollbar thumb
+ &::-webkit-scrollbar-thumb {
+ background-color: var(--md-default-fg-color--lighter);
+
+ // Webkit scrollbar thumb on hover
+ &:hover {
+ background-color: var(--md-accent-fg-color);
+ }
+ }
+
+ // Icon search result metadata
+ &__meta {
+ position: absolute;
+ top: px2rem(8px);
+ right: px2rem(12px);
+ font-size: px2rem(12.8px);
+ color: var(--md-default-fg-color--lighter);
+ }
+
+ // Icon search result list
+ &__list {
+ padding: 0;
+ margin: 0;
+ // Hack: necessary because of increased specificity due to the PostCSS
+ // plugin which prefixes this with `[dir=...]` selectors.
+ margin-inline-start: 0;
+ list-style: none;
+ }
+
+ // Icon search result item
+ &__item {
+ padding: px2rem(4px) px2rem(12px);
+ margin: 0;
+ // Hack: necessary because of increased specificity due to the PostCSS
+ // plugin which prefixes this with `[dir=...]` selectors.
+ margin-inline-start: 0;
+ border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest);
+
+ // Omit border on last child
+ &:last-child {
+ border-bottom: none;
+ }
+
+ // Item content
+ > * {
+ margin-right: px2rem(12px);
+ }
+
+ // Set icon dimensions to fit
+ img {
+ width: px2rem(18px);
+ height: px2rem(18px);
+
+ // Slate theme, i.e. dark mode
+ [data-md-color-scheme="slate"] &[src*="squidfunk"] {
+ filter: invert(1); /* stylelint-disable-line */
+ }
+ }
+ }
+ }
+}
diff --git a/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss b/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss
new file mode 100644
index 00000000..e2b16570
--- /dev/null
+++ b/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss
@@ -0,0 +1,128 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Premium sponsors
+ .mdx-premium {
+
+ // Paragraphs
+ p {
+ margin: 2em 0;
+ text-align: center;
+ }
+
+ // Premium sponsor image
+ img {
+ height: px2rem(65px);
+ }
+
+ // Premium sponsor list
+ p:last-child {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ // Premium sponsor link
+ > a {
+ display: block;
+ flex-shrink: 0;
+ }
+ }
+ }
+
+ // Sponsorship
+ .mdx-sponsorship {
+
+ // Sponsorship list
+ &__list {
+ margin: 2em 0;
+
+ // Clearfix, because we can't use overflow: auto
+ &::after {
+ display: block;
+ clear: both;
+ content: "";
+ }
+ }
+
+ // Sponsorship item
+ &__item {
+ display: block;
+ float: inline-start;
+ width: px2rem(32px);
+ height: px2rem(32px);
+ margin: px2rem(4px);
+ overflow: hidden;
+ border-radius: 100%;
+ transition:
+ color 125ms,
+ transform 125ms;
+ transform: scale(1);
+
+ // Sponsor item on focus/hover
+ &:is(:focus, :hover) {
+ transform: scale(1.1);
+
+ // Sponsor avatar
+ img {
+ filter: grayscale(0%);
+ }
+ }
+
+ // Private sponsor
+ &--private {
+ font-size: px2rem(12px);
+ font-weight: 700;
+ line-height: px2rem(32px);
+ color: var(--md-default-fg-color--lighter);
+ text-align: center;
+ background: var(--md-default-fg-color--lightest);
+ }
+
+ // Sponsor avatar
+ img {
+ display: block;
+ width: 100%;
+ height: auto;
+ filter: grayscale(100%) opacity(75%);
+ transition: filter 125ms;
+ }
+ }
+ }
+
+ // Sponsorship button
+ .mdx-sponsorship-button {
+ font-weight: 400;
+ }
+
+ // Sponsorship count and total
+ .mdx-sponsorship-count,
+ .mdx-sponsorship-total {
+ font-weight: 700;
+ }
+}
diff --git a/src/overrides/home.html b/src/overrides/home.html
new file mode 100644
index 00000000..3f54ca82
--- /dev/null
+++ b/src/overrides/home.html
@@ -0,0 +1,106 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+{% extends "main.html" %}
+
+<!-- Render hero under tabs -->
+{% block tabs %}
+ {{ super() }}
+
+ <!-- Additional styles for landing page -->
+ <style>
+
+ /* Application header should be static for the landing page */
+ .md-header {
+ position: initial;
+ }
+
+ /* Remove spacing, as we cannot hide it completely */
+ .md-main__inner {
+ margin: 0;
+ }
+
+ /* Hide main content for now */
+ .md-content {
+ display: none;
+ }
+
+ /* Hide table of contents */
+ @media screen and (min-width: 60em) {
+ .md-sidebar--secondary {
+ display: none;
+ }
+ }
+
+ /* Hide navigation */
+ @media screen and (min-width: 76.25em) {
+ .md-sidebar--primary {
+ display: none;
+ }
+ }
+ </style>
+
+ <!-- Hero for landing page -->
+ <section class="mdx-container">
+ <div class="md-grid md-typeset">
+ <div class="mdx-hero">
+
+ <!-- Hero image -->
+ <div class="mdx-hero__image">
+ <img
+ src="assets/images/illustration.png"
+ alt=""
+ width="1659"
+ height="1200"
+ draggable="false"
+ >
+ </div>
+
+ <!-- Hero content -->
+ <div class="mdx-hero__content">
+ <h1>Technical documentation that just works</h1>
+ <p>{{ config.site_description }}. Set up in 5 minutes.</p>
+ <a
+ href="{{ page.next_page.url | url }}"
+ title="{{ page.next_page.title | e }}"
+ class="md-button md-button--primary"
+ >
+ Quick start
+ </a>
+ <a
+ href="{{ 'insiders/' | url }}"
+ title="Material for MkDocs Insiders"
+ class="md-button"
+ >
+ Get Insiders
+ </a>
+ </div>
+ </div>
+ </div>
+ </section>
+{% endblock %}
+
+<!-- Content -->
+{% block content %}{% endblock %}
+
+<!-- Application footer -->
+{% block footer %}{% endblock %}
diff --git a/src/overrides/hooks/shortcodes.py b/src/overrides/hooks/shortcodes.py
new file mode 100644
index 00000000..5b02e3cf
--- /dev/null
+++ b/src/overrides/hooks/shortcodes.py
@@ -0,0 +1,283 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from __future__ import annotations
+
+import posixpath
+import re
+
+from mkdocs.config.defaults import MkDocsConfig
+from mkdocs.structure.files import File, Files
+from mkdocs.structure.pages import Page
+from re import Match
+
+# -----------------------------------------------------------------------------
+# Hooks
+# -----------------------------------------------------------------------------
+
+# @todo
+def on_page_markdown(
+ markdown: str, *, page: Page, config: MkDocsConfig, files: Files
+):
+
+ # Replace callback
+ def replace(match: Match):
+ type, args = match.groups()
+ args = args.strip()
+ if type == "version":
+ if args.startswith("insiders-"):
+ return _badge_for_version_insiders(args, page, files)
+ else:
+ return _badge_for_version(args, page, files)
+ elif type == "sponsors": return _badge_for_sponsors(page, files)
+ elif type == "flag": return flag(args, page, files)
+ elif type == "option": return option(args)
+ elif type == "setting": return setting(args)
+ elif type == "feature": return _badge_for_feature(args, page, files)
+ elif type == "plugin": return _badge_for_plugin(args, page, files)
+ elif type == "extension": return _badge_for_extension(args, page, files)
+ elif type == "utility": return _badge_for_utility(args, page, files)
+ elif type == "example": return _badge_for_example(args, page, files)
+ elif type == "default":
+ if args == "none": return _badge_for_default_none(page, files)
+ elif args == "computed": return _badge_for_default_computed(page, files)
+ else: return _badge_for_default(args, page, files)
+
+ # Otherwise, raise an error
+ raise RuntimeError(f"Unknown shortcode: {type}")
+
+ # Find and replace all external asset URLs in current page
+ return re.sub(
+ r"<!-- md:(\w+)(.*?) -->",
+ replace, markdown, flags = re.I | re.M
+ )
+
+# -----------------------------------------------------------------------------
+# Helper functions
+# -----------------------------------------------------------------------------
+
+# Create a flag of a specific type
+def flag(args: str, page: Page, files: Files):
+ type, *_ = args.split(" ", 1)
+ if type == "experimental": return _badge_for_experimental(page, files)
+ elif type == "required": return _badge_for_required(page, files)
+ elif type == "customization": return _badge_for_customization(page, files)
+ elif type == "metadata": return _badge_for_metadata(page, files)
+ elif type == "multiple": return _badge_for_multiple(page, files)
+ raise RuntimeError(f"Unknown type: {type}")
+
+# Create a linkable option
+def option(type: str):
+ _, *_, name = re.split(r"[.:]", type)
+ return f"[`{name}`](#+{type}){{ #+{type} }}\n\n"
+
+# Create a linkable setting - @todo append them to the bottom of the page
+def setting(type: str):
+ _, *_, name = re.split(r"[.*]", type)
+ return f"`{name}` {{ #{type} }}\n\n[{type}]: #{type}\n\n"
+
+# -----------------------------------------------------------------------------
+
+# Resolve path of file relative to given page - the posixpath always includes
+# one additional level of `..` which we need to remove
+def _resolve_path(path: str, page: Page, files: Files):
+ path, anchor, *_ = f"{path}#".split("#")
+ path = _resolve(files.get_file_from_path(path), page)
+ return "#".join([path, anchor]) if anchor else path
+
+# Resolve path of file relative to given page - the posixpath always includes
+# one additional level of `..` which we need to remove
+def _resolve(file: File, page: Page):
+ path = posixpath.relpath(file.src_uri, page.file.src_uri)
+ return posixpath.sep.join(path.split(posixpath.sep)[1:])
+
+# -----------------------------------------------------------------------------
+
+# Create badge
+def _badge(icon: str, text: str = "", type: str = ""):
+ classes = f"mdx-badge mdx-badge--{type}" if type else "mdx-badge"
+ return "".join([
+ f"<span class=\"{classes}\">",
+ *([f"<span class=\"mdx-badge__icon\">{icon}</span>"] if icon else []),
+ *([f"<span class=\"mdx-badge__text\">{text}</span>"] if text else []),
+ f"</span>",
+ ])
+
+# Create sponsors badge
+def _badge_for_sponsors(page: Page, files: Files):
+ icon = "material-heart"
+ href = _resolve_path("insiders/index.md", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Sponsors only')",
+ type = "heart"
+ )
+
+# Create badge for version
+def _badge_for_version(text: str, page: Page, files: Files):
+ spec = text
+ path = f"changelog/index.md#{spec}"
+
+ # Return badge
+ icon = "material-tag-outline"
+ href = _resolve_path("conventions.md#version", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Minimum version')",
+ text = f"[{text}]({_resolve_path(path, page, files)})" if spec else ""
+ )
+
+# Create badge for version of Insiders
+def _badge_for_version_insiders(text: str, page: Page, files: Files):
+ spec = text.replace("insiders-", "")
+ path = f"insiders/changelog/index.md#{spec}"
+
+ # Return badge
+ icon = "material-tag-heart-outline"
+ href = _resolve_path("conventions.md#version-insiders", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Minimum version')",
+ text = f"[{text}]({_resolve_path(path, page, files)})" if spec else ""
+ )
+
+# Create badge for feature
+def _badge_for_feature(text: str, page: Page, files: Files):
+ icon = "material-toggle-switch"
+ href = _resolve_path("conventions.md#feature", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Optional feature')",
+ text = text
+ )
+
+# Create badge for plugin
+def _badge_for_plugin(text: str, page: Page, files: Files):
+ icon = "material-floppy"
+ href = _resolve_path("conventions.md#plugin", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Plugin')",
+ text = text
+ )
+
+# Create badge for extension
+def _badge_for_extension(text: str, page: Page, files: Files):
+ icon = "material-language-markdown"
+ href = _resolve_path("conventions.md#extension", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Markdown extension')",
+ text = text
+ )
+
+# Create badge for utility
+def _badge_for_utility(text: str, page: Page, files: Files):
+ icon = "material-package-variant"
+ href = _resolve_path("conventions.md#utility", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Third-party utility')",
+ text = text
+ )
+
+# Create badge for example
+def _badge_for_example(text: str, page: Page, files: Files):
+ return "\n".join([
+ _badge_for_example_download(text, page, files),
+ _badge_for_example_view(text, page, files)
+ ])
+
+# Create badge for example view
+def _badge_for_example_view(text: str, page: Page, files: Files):
+ icon = "material-folder-eye"
+ href = f"https://mkdocs-material.github.io/examples/{text}/"
+ return _badge(
+ icon = f"[:{icon}:]({href} 'View example')",
+ type = "right"
+ )
+
+# Create badge for example download
+def _badge_for_example_download(text: str, page: Page, files: Files):
+ icon = "material-folder-download"
+ href = f"https://mkdocs-material.github.io/examples/{text}.zip"
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Download example')",
+ text = f"[`.zip`]({href})",
+ type = "right"
+ )
+
+# Create badge for default value
+def _badge_for_default(text: str, page: Page, files: Files):
+ icon = "material-water"
+ href = _resolve_path("conventions.md#default", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Default value')",
+ text = text
+ )
+
+# Create badge for empty default value
+def _badge_for_default_none(page: Page, files: Files):
+ icon = "material-water-outline"
+ href = _resolve_path("conventions.md#default", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Default value is empty')"
+ )
+
+# Create badge for computed default value
+def _badge_for_default_computed(page: Page, files: Files):
+ icon = "material-water-check"
+ href = _resolve_path("conventions.md#default", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Default value is computed')"
+ )
+
+# Create badge for metadata property flag
+def _badge_for_metadata(page: Page, files: Files):
+ icon = "material-list-box-outline"
+ href = _resolve_path("conventions.md#metadata", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Metadata property')"
+ )
+
+# Create badge for required value flag
+def _badge_for_required(page: Page, files: Files):
+ icon = "material-alert"
+ href = _resolve_path("conventions.md#required", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Required value')"
+ )
+
+# Create badge for customization flag
+def _badge_for_customization(page: Page, files: Files):
+ icon = "material-brush-variant"
+ href = _resolve_path("conventions.md#customization", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Customization')"
+ )
+
+# Create badge for multiple instance flag
+def _badge_for_multiple(page: Page, files: Files):
+ icon = "material-inbox-multiple"
+ href = _resolve_path("conventions.md#multiple-instances", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Multiple instances')"
+ )
+
+# Create badge for experimental flag
+def _badge_for_experimental(page: Page, files: Files):
+ icon = "material-flask-outline"
+ href = _resolve_path("conventions.md#experimental", page, files)
+ return _badge(
+ icon = f"[:{icon}:]({href} 'Experimental')"
+ )
diff --git a/src/overrides/hooks/translations.html b/src/overrides/hooks/translations.html
new file mode 100644
index 00000000..ab41c77d
--- /dev/null
+++ b/src/overrides/hooks/translations.html
@@ -0,0 +1,54 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Render translation language -->
+{% macro render_language(language) %}
+ <div class="mdx-flags__item" markdown>
+ :flag_{{ language.flag }}:{ .lg .middle }
+ <span class="mdx-flags__content">
+ <span>
+ <strong>{{ language.name }}</strong>
+ <code>{{ language.code }}</code>
+ </span>
+ {% if language.miss %}
+ <span>
+ <a href="{{ language.link }}">
+ {{ language.miss | length }} translations missing
+ </a>
+ </span>
+ {% else %}
+ <small>Complete</small>
+ {% endif %}
+ </span>
+ </div>
+{% endmacro %}
+
+<!-- Render translations -->
+{% macro render(translations, start = 1) %}
+ <div class="mdx-columns mdx-flags" markdown>
+ <ol markdown>
+ {% for language in translations %}
+ <li markdown>{{ render_language(language) }}</li>
+ {% endfor %}
+ </ol>
+ </div>
+{% endmacro %}
diff --git a/src/overrides/hooks/translations.py b/src/overrides/hooks/translations.py
new file mode 100644
index 00000000..661fd18e
--- /dev/null
+++ b/src/overrides/hooks/translations.py
@@ -0,0 +1,193 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import os
+import re
+
+from glob import iglob
+from mkdocs.config.defaults import MkDocsConfig
+from mkdocs.structure.pages import Page
+from urllib.parse import urlencode, urlparse
+
+# -----------------------------------------------------------------------------
+# Hooks
+# -----------------------------------------------------------------------------
+
+# Determine missing translations and render language overview in the setup
+# guide, including links to provide missing translations.
+def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files):
+ issue_url = "https://github.com/squidfunk/mkdocs-material/issues/new"
+ if page.file.src_uri != "setup/changing-the-language.md":
+ return
+
+ # Collect all existing languages
+ names: dict[str, str] = {}
+ known: dict[str, dict[str, str]] = {}
+ for path in iglob("src/templates/partials/languages/*.html"):
+ with open(path, "r", encoding = "utf-8") as f:
+ data = f.read()
+
+ # Extract language code and name
+ name, = re.findall(r"<!-- Translations: (.+) -->", data)
+ code, _ = os.path.splitext(os.path.basename(path))
+
+ # Map names and available translations
+ names[code] = name
+ known[code] = dict(re.findall(
+ r"^ \"([^\"]+)\": \"([^\"]*)\"(?:,|$)?", data,
+ re.MULTILINE
+ ))
+
+ # Remove technical stuff
+ for key in [
+ "direction",
+ "search.config.pipeline",
+ "search.config.lang",
+ "search.config.separator"
+ ]:
+ if key in known[code]:
+ del known[code][key]
+
+ # Traverse all languages and compute missing translations
+ languages = []
+ reference = set(known["en"])
+ for code, name in names.items():
+ miss = reference - set(known[code])
+
+ # Check each translations
+ translations: list[str] = []
+ for key, value in known["en"].items():
+ if key in known[code]:
+ translations.append(
+ f" \"{key}\": \"{known[code][key]}\""
+ )
+ else:
+ translations.append(
+ f" \"{key}\": \"{value} ⬅️\""
+ )
+
+ # Assemble GitHub issue URL
+ link = urlparse(issue_url)
+ link = link._replace(query = urlencode({
+ "template": "04-add-translations.yml",
+ "title": f"Update {name} translations",
+ "translations": "\n".join([
+ "{% macro t(key) %}{{ {",
+ ",\n".join(translations),
+ "}[key] }}{% endmacro %}"
+ ]),
+ "country-flag": f":flag_{countries[code]}:"
+ }))
+
+ # Add translation
+ languages.append({
+ "flag": countries[code],
+ "code": code,
+ "name": name,
+ "link": link.geturl(),
+ "miss": miss
+ })
+
+ # Load template and render translations
+ env = config.theme.get_env()
+ template = env.get_template( "hooks/translations.html")
+ translations = template.module.render(
+ sorted(languages, key = lambda language: language["name"])
+ )
+
+ # Replace translation marker
+ return markdown.replace(
+ "<!-- hooks/translations.py -->", "\n".join(
+ [line.lstrip() for line in translations.split("\n")
+ ]
+ ))
+
+# -----------------------------------------------------------------------------
+# Data
+# -----------------------------------------------------------------------------
+
+# Map ISO 639-1 (languages) to ISO 3166 (countries)
+countries = dict({
+ "af": "za",
+ "ar": "ae",
+ "be": "by",
+ "bg": "bg",
+ "bn": "bd",
+ "ca": "es",
+ "cs": "cz",
+ "da": "dk",
+ "de": "de",
+ "el": "gr",
+ "en": "us",
+ "eo": "eu",
+ "es": "es",
+ "et": "ee",
+ "eu": "es",
+ "fa": "ir",
+ "fi": "fi",
+ "fr": "fr",
+ "gl": "es",
+ "he": "il",
+ "hi": "in",
+ "hr": "hr",
+ "hu": "hu",
+ "hy": "am",
+ "id": "id",
+ "is": "is",
+ "it": "it",
+ "ja": "jp",
+ "ka": "ge",
+ "kn": "in",
+ "ko": "kr",
+ "ku-IQ": "iq",
+ "lb": "lu",
+ "lt": "lt",
+ "lv": "lv",
+ "mk": "mk",
+ "mn": "mn",
+ "ms": "my",
+ "my": "mm",
+ "nb": "no",
+ "nl": "nl",
+ "nn": "no",
+ "pl": "pl",
+ "pt-BR": "br",
+ "pt": "pt",
+ "ro": "ro",
+ "ru": "ru",
+ "sa": "in",
+ "sh": "rs",
+ "si": "lk",
+ "sk": "sk",
+ "sl": "si",
+ "sr": "rs",
+ "sv": "se",
+ "te": "in",
+ "th": "th",
+ "tl": "ph",
+ "tr": "tr",
+ "uk": "ua",
+ "ur": "pk",
+ "uz": "uz",
+ "vi": "vn",
+ "zh": "cn",
+ "zh-Hant": "cn",
+ "zh-TW": "tw"
+})
diff --git a/src/overrides/main.html b/src/overrides/main.html
new file mode 100644
index 00000000..39b68b5a
--- /dev/null
+++ b/src/overrides/main.html
@@ -0,0 +1,59 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+{% extends "base.html" %}
+
+<!-- Custom front matter -->
+{% block extrahead %}
+
+ <!-- Extra style sheets (can't be set in mkdocs.yml due to content hash) -->
+ <link
+ rel="stylesheet"
+ href="{{ 'assets/stylesheets/custom.css' | url }}"
+ />
+{% endblock %}
+
+<!-- Announcement bar -->
+{% block announce %}
+ For updates follow <strong>@squidfunk</strong> on
+ <a rel="me" href="https://fosstodon.org/@squidfunk">
+ <span class="twemoji mastodon">
+ {% include ".icons/fontawesome/brands/mastodon.svg" %}
+ </span>
+ <strong>Fosstodon</strong>
+ </a>
+ and
+ <a href="https://twitter.com/squidfunk">
+ <span class="twemoji twitter">
+ {% include ".icons/fontawesome/brands/twitter.svg" %}
+ </span>
+ <strong>Twitter</strong>
+ </a>
+{% endblock %}
+
+<!-- Theme-related JavaScript -->
+{% block scripts %}
+ {{ super() }}
+
+ <!-- Extra JavaScript (can't be set in mkdocs.yml due to content hash) -->
+ <script src="{{ 'assets/javascripts/custom.js' | url }}"></script>
+{% endblock %}
diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py
new file mode 100644
index 00000000..d1899378
--- /dev/null
+++ b/src/plugins/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
diff --git a/src/plugins/blog/__init__.py b/src/plugins/blog/__init__.py
new file mode 100644
index 00000000..d1899378
--- /dev/null
+++ b/src/plugins/blog/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
diff --git a/src/plugins/blog/author.py b/src/plugins/blog/author.py
new file mode 100644
index 00000000..1dcfc2de
--- /dev/null
+++ b/src/plugins/blog/author.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from mkdocs.config.base import Config
+from mkdocs.config.config_options import DictOfItems, SubConfig, Type
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Author
+class Author(Config):
+ name = Type(str)
+ description = Type(str)
+ avatar = Type(str)
+
+# -----------------------------------------------------------------------------
+
+# Authors
+class Authors(Config):
+ authors = DictOfItems(SubConfig(Author), default = {})
diff --git a/src/plugins/blog/config.py b/src/plugins/blog/config.py
new file mode 100644
index 00000000..c7a85095
--- /dev/null
+++ b/src/plugins/blog/config.py
@@ -0,0 +1,88 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from functools import partial
+from markdown.extensions.toc import slugify
+from mkdocs.config.config_options import Choice, Deprecated, Optional, Type
+from mkdocs.config.base import Config
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Blog plugin configuration
+class BlogConfig(Config):
+ enabled = Type(bool, default = True)
+
+ # Settings for blog
+ blog_dir = Type(str, default = "blog")
+ blog_toc = Type(bool, default = False)
+
+ # Settings for posts
+ post_dir = Type(str, default = "{blog}/posts")
+ post_date_format = Type(str, default = "long")
+ post_url_date_format = Type(str, default = "yyyy/MM/dd")
+ post_url_format = Type(str, default = "{date}/{slug}")
+ post_url_max_categories = Type(int, default = 1)
+ post_slugify = Type((type(slugify), partial), default = slugify)
+ post_slugify_separator = Type(str, default = "-")
+ post_excerpt = Choice(["optional", "required"], default = "optional")
+ post_excerpt_max_authors = Type(int, default = 1)
+ post_excerpt_max_categories = Type(int, default = 5)
+ post_excerpt_separator = Type(str, default = "<!-- more -->")
+ post_readtime = Type(bool, default = True)
+ post_readtime_words_per_minute = Type(int, default = 265)
+
+ # Settings for archive
+ archive = Type(bool, default = True)
+ archive_name = Type(str, default = "blog.archive")
+ archive_date_format = Type(str, default = "yyyy")
+ archive_url_date_format = Type(str, default = "yyyy")
+ archive_url_format = Type(str, default = "archive/{date}")
+ archive_toc = Optional(Type(bool))
+
+ # Settings for categories
+ categories = Type(bool, default = True)
+ categories_name = Type(str, default = "blog.categories")
+ categories_url_format = Type(str, default = "category/{slug}")
+ categories_slugify = Type((type(slugify), partial), default = slugify)
+ categories_slugify_separator = Type(str, default = "-")
+ categories_allowed = Type(list, default = [])
+ categories_toc = Optional(Type(bool))
+
+ # Settings for pagination
+ pagination = Type(bool, default = True)
+ pagination_per_page = Type(int, default = 10)
+ pagination_url_format = Type(str, default = "page/{page}")
+ pagination_format = Type(str, default = "~2~")
+ pagination_if_single_page = Type(bool, default = False)
+ pagination_keep_content = Type(bool, default = False)
+
+ # Settings for authors
+ authors = Type(bool, default = True)
+ authors_file = Type(str, default = "{blog}/.authors.yml")
+
+ # Settings for drafts
+ draft = Type(bool, default = False)
+ draft_on_serve = Type(bool, default = True)
+ draft_if_future_date = Type(bool, default = False)
+
+ # Deprecated settings
+ pagination_template = Deprecated(moved_to = "pagination_format")
diff --git a/src/plugins/blog/plugin.py b/src/plugins/blog/plugin.py
new file mode 100644
index 00000000..375b8cfe
--- /dev/null
+++ b/src/plugins/blog/plugin.py
@@ -0,0 +1,884 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from __future__ import annotations
+
+import logging
+import os
+import posixpath
+import yaml
+
+from babel.dates import format_date
+from datetime import datetime
+from mkdocs.config.defaults import MkDocsConfig
+from mkdocs.exceptions import PluginError
+from mkdocs.plugins import BasePlugin, event_priority
+from mkdocs.structure import StructureItem
+from mkdocs.structure.files import File, Files, InclusionLevel
+from mkdocs.structure.nav import Navigation, Section
+from mkdocs.structure.pages import Page
+from mkdocs.utils import copy_file, get_relative_url
+from paginate import Page as Pagination
+from shutil import rmtree
+from tempfile import mkdtemp
+from yaml import SafeLoader
+
+from .author import Authors
+from .config import BlogConfig
+from .readtime import readtime
+from .structure import Archive, Category, Excerpt, Post, View
+from .templates import url_filter
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Blog plugin
+class BlogPlugin(BasePlugin[BlogConfig]):
+ supports_multiple_instances = True
+
+ # Initialize plugin
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Initialize incremental builds
+ self.is_serve = False
+ self.is_dirty = False
+
+ # Initialize temporary directory
+ self.temp_dir = mkdtemp()
+
+ # Determine whether we're serving the site
+ def on_startup(self, *, command, dirty):
+ self.is_serve = command == "serve"
+ self.is_dirty = dirty
+
+ # Initialize authors and set defaults
+ def on_config(self, config):
+ if not self.config.enabled:
+ return
+
+ # Initialize entrypoint
+ self.blog: View
+
+ # Initialize and resolve authors, if enabled
+ if self.config.authors:
+ self.authors = self._resolve_authors(config)
+
+ # Initialize table of contents settings
+ if not isinstance(self.config.archive_toc, bool):
+ self.config.archive_toc = self.config.blog_toc
+ if not isinstance(self.config.categories_toc, bool):
+ self.config.categories_toc = self.config.blog_toc
+
+ # By default, drafts are rendered when the documentation is served,
+ # but not when it is built, for a better user experience
+ if self.is_serve and self.config.draft_on_serve:
+ self.config.draft = True
+
+ # Resolve and load posts and generate views (run later) - we want to allow
+ # other plugins to add generated posts or views, so we run this plugin as
+ # late as possible. We also need to remove the posts from the navigation
+ # before navigation is constructed, as the entrypoint should be considered
+ # to be the active page for each post. The URLs of posts are computed before
+ # Markdown processing, so that when linking to and from posts, behavior is
+ # exactly the same as with regular documentation pages. We create all pages
+ # related to posts as part of this plugin, so we control the entire process.
+ @event_priority(-50)
+ def on_files(self, files, *, config):
+ if not self.config.enabled:
+ return
+
+ # Resolve path to entrypoint and site directory
+ root = posixpath.normpath(self.config.blog_dir)
+ site = config.site_dir
+
+ # Compute path to posts directory
+ path = self.config.post_dir.format(blog = root)
+ path = posixpath.normpath(path)
+
+ # Adjust destination paths for media files
+ for file in files.media_files():
+ if not file.src_uri.startswith(path):
+ continue
+
+ # We need to adjust destination paths for assets to remove the
+ # purely functional posts directory prefix when building
+ file.dest_uri = file.dest_uri.replace(path, root)
+ file.abs_dest_path = os.path.join(site, file.dest_path)
+ file.url = file.url.replace(path, root)
+
+ # Resolve entrypoint and posts sorted by descending date - if the posts
+ # directory or entrypoint do not exist, they are automatically created
+ self.blog = self._resolve(files, config)
+ self.blog.posts = sorted(
+ self._resolve_posts(files, config),
+ key = lambda post: post.config.date.created,
+ reverse = True
+ )
+
+ # Generate views for archive
+ if self.config.archive:
+ views = self._generate_archive(config, files)
+ self.blog.views.extend(views)
+
+ # Generate views for categories
+ if self.config.categories:
+ views = self._generate_categories(config, files)
+ self.blog.views.extend(views)
+
+ # Generate pages for views
+ if self.config.pagination:
+ for view in self._resolve_views(self.blog):
+ for page in self._generate_pages(view, config, files):
+ page.file.inclusion = InclusionLevel.EXCLUDED
+ view.pages.append(page)
+
+ # Ensure that entrypoint is always included in navigation
+ self.blog.file.inclusion = InclusionLevel.INCLUDED
+
+ # Attach posts and views to navigation (run later) - again, we allow other
+ # plugins to alter the navigation before we start to attach posts and views
+ # generated by this plugin at the correct locations in the navigation. Also,
+ # we make sure to correct links to the parent and siblings of each page.
+ @event_priority(-50)
+ def on_nav(self, nav, *, config, files):
+ if not self.config.enabled:
+ return
+
+ # If we're not building a standalone blog, the entrypoint will always
+ # have a parent when it is included in the navigation. The parent is
+ # essential to correctly resolve the location where the archive and
+ # category views are attached. If the entrypoint doesn't have a parent,
+ # we know that the author did not include it in the navigation, so we
+ # explicitly mark it as not included.
+ if not self.blog.parent and self.config.blog_dir != ".":
+ self.blog.file.inclusion = InclusionLevel.NOT_IN_NAV
+
+ # Attach posts to entrypoint without adding them to the navigation, so
+ # that the entrypoint is considered to be the active page for each post
+ self._attach(self.blog, [None, *reversed(self.blog.posts), None])
+ for post in self.blog.posts:
+ post.file.inclusion = InclusionLevel.NOT_IN_NAV
+
+ # Revert temporary exclusion of views from navigation
+ for view in self._resolve_views(self.blog):
+ for page in view.pages:
+ page.file.inclusion = self.blog.file.inclusion
+
+ # Attach views for archive
+ if self.config.archive:
+ title = self._translate(self.config.archive_name, config)
+ views = [_ for _ in self.blog.views if isinstance(_, Archive)]
+
+ # Attach and link views for archive
+ if self.blog.file.inclusion.is_in_nav():
+ self._attach_to(self.blog, Section(title, views), nav)
+
+ # Attach views for categories
+ if self.config.categories:
+ title = self._translate(self.config.categories_name, config)
+ views = [_ for _ in self.blog.views if isinstance(_, Category)]
+
+ # Attach and link views for categories, if any
+ if self.blog.file.inclusion.is_in_nav() and views:
+ self._attach_to(self.blog, Section(title, views), nav)
+
+ # Attach pages for views
+ if self.config.pagination:
+ for view in self._resolve_views(self.blog):
+ for at in range(1, len(view.pages)):
+ self._attach_at(view.parent, view, view.pages[at])
+
+ # Prepare post for rendering (run later) - allow other plugins to alter
+ # the contents or metadata of a post before it is rendered and make sure
+ # that the post includes a separator, which is essential for rendering
+ # excerpts that should be included in views
+ @event_priority(-50)
+ def on_page_markdown(self, markdown, *, page, config, files):
+ if not self.config.enabled:
+ return
+
+ # Skip if page is not a post managed by this instance - this plugin has
+ # support for multiple instances, which is why this check is necessary
+ if page not in self.blog.posts:
+ if not self.config.pagination:
+ return
+
+ # We set the contents of the view to its title if pagination should
+ # not keep the content of the original view on paginated views
+ if not self.config.pagination_keep_content:
+ view = self._resolve_original(page)
+ if view in self._resolve_views(self.blog):
+
+ # If the current view is paginated, use the rendered title
+ # of the original view in case the author set the title in
+ # the page's contents, or it would be overridden with the
+ # one set in mkdocs.yml, leading to inconsistent headings
+ assert isinstance(view, View)
+ if view != page:
+ name = view._title_from_render or view.title
+ return f"# {name}"
+
+ # Nothing more to be done for views
+ return
+
+ # Extract and assign authors to post, if enabled
+ if self.config.authors:
+ for name in page.config.authors:
+ if name not in self.authors:
+ raise PluginError(f"Couldn't find author '{name}'")
+
+ # Append to list of authors
+ page.authors.append(self.authors[name])
+
+ # Extract settings for excerpts
+ separator = self.config.post_excerpt_separator
+ max_authors = self.config.post_excerpt_max_authors
+ max_categories = self.config.post_excerpt_max_categories
+
+ # Ensure presence of separator and throw, if its absent and required -
+ # we append the separator to the end of the contents of the post, if it
+ # is not already present, so we can remove footnotes or other content
+ # from the excerpt without affecting the content of the excerpt
+ if separator not in page.markdown:
+ path = page.file.src_path
+ if self.config.post_excerpt == "required":
+ raise PluginError(
+ f"Couldn't find '{separator}' separator in '{path}'"
+ )
+ else:
+ page.markdown += f"\n\n{separator}"
+
+ # Create excerpt for post and inherit authors and categories - excerpts
+ # can contain a subset of the authors and categories of the post
+ page.excerpt = Excerpt(page, config, files)
+ page.excerpt.authors = page.authors[:max_authors]
+ page.excerpt.categories = page.categories[:max_categories]
+
+ # Process posts
+ def on_page_content(self, html, *, page, config, files):
+ if not self.config.enabled:
+ return
+
+ # Skip if page is not a post managed by this instance - this plugin has
+ # support for multiple instances, which is why this check is necessary
+ if page not in self.blog.posts:
+ return
+
+ # Compute readtime of post, if enabled and not explicitly set
+ if self.config.post_readtime:
+ words_per_minute = self.config.post_readtime_words_per_minute
+ if not page.config.readtime:
+ page.config.readtime = readtime(html, words_per_minute)
+
+ # Register template filters for plugin
+ def on_env(self, env, *, config, files):
+ if not self.config.enabled:
+ return
+
+ # Filter for formatting dates related to posts
+ def date_filter(date: datetime):
+ return self._format_date_for_post(date, config)
+
+ # Register custom template filters
+ env.filters["date"] = date_filter
+ env.filters["url"] = url_filter
+
+ # Prepare view for rendering (run latest) - views are rendered last, as we
+ # need to mutate the navigation to account for pagination. The main problem
+ # is that we need to replace the view in the navigation, because otherwise
+ # the view would not be considered active.
+ @event_priority(-100)
+ def on_page_context(self, context, *, page, config, nav):
+ if not self.config.enabled:
+ return
+
+ # Skip if page is not a view managed by this instance - this plugin has
+ # support for multiple instances, which is why this check is necessary
+ view = self._resolve_original(page)
+ if view not in self._resolve_views(self.blog):
+ return
+
+ # If the current view is paginated, replace and rewire it - the current
+ # view temporarily becomes the main view, and is reset after rendering
+ assert isinstance(view, View)
+ if view != page:
+ prev = view.pages[view.pages.index(page) - 1]
+
+ # Replace previous page with current page
+ items = self._resolve_siblings(view, nav)
+ items[items.index(prev)] = page
+
+ # Render excerpts and prepare pagination
+ posts, pagination = self._render(page)
+
+ # Render pagination links
+ def pager(args: object):
+ return pagination.pager(
+ format = self.config.pagination_format,
+ show_if_single_page = self.config.pagination_if_single_page,
+ **args
+ )
+
+ # Assign posts and pagination to context
+ context["posts"] = posts
+ context["pagination"] = pager if pagination else None
+
+ # After rendering a paginated view, replace the URL of the paginated view
+ # with the URL of the original view - since we need to replace the original
+ # view with a paginated view in `on_page_context` for correct resolution of
+ # the active state, we must fix the paginated view URLs after rendering
+ def on_post_page(self, output, *, page, config):
+ if not self.config.enabled:
+ return
+
+ # Skip if page is not a view managed by this instance - this plugin has
+ # support for multiple instances, which is why this check is necessary
+ view = self._resolve_original(page)
+ if view not in self._resolve_views(self.blog):
+ return
+
+ # If the current view is paginated, replace the URL of the paginated
+ # view with the URL of the original view - see https://t.ly/Yeh-P
+ assert isinstance(view, View)
+ if view != page:
+ page.file.url = view.file.url
+
+ # Remove temporary directory on shutdown
+ def on_shutdown(self):
+ rmtree(self.temp_dir)
+
+ # -------------------------------------------------------------------------
+
+ # Check if the given post is excluded
+ def _is_excluded(self, post: Post):
+ if self.config.draft:
+ return False
+
+ # If a post was not explicitly marked or unmarked as draft, and the
+ # date should be taken into account, we automatically mark it as draft
+ # if the publishing date is in the future. This, of course, is opt-in
+ # and must be explicitly enabled by the author.
+ if not isinstance(post.config.draft, bool):
+ if self.config.draft_if_future_date:
+ return post.config.date.created > datetime.now()
+
+ # Post might be a draft
+ return bool(post.config.draft)
+
+ # -------------------------------------------------------------------------
+
+ # Resolve entrypoint - the entrypoint of the blog must have been created
+ # if it did not exist before, and hosts all posts sorted by descending date
+ def _resolve(self, files: Files, config: MkDocsConfig):
+ path = os.path.join(self.config.blog_dir, "index.md")
+ path = os.path.normpath(path)
+
+ # Create entrypoint, if it does not exist - note that the entrypoint is
+ # created in the docs directory, not in the temporary directory
+ docs = os.path.relpath(config.docs_dir)
+ name = os.path.join(docs, path)
+ if not os.path.isfile(name):
+ file = self._path_to_file(path, config, temp = False)
+ files.append(file)
+
+ # Create file in docs directory
+ self._save_to_file(file.abs_src_path, "# Blog\n\n")
+
+ # Create and return entrypoint
+ file = files.get_file_from_path(path)
+ return View(None, file, config)
+
+ # Resolve post - the caller must make sure that the given file points to an
+ # actual post (and not a page), or behavior might be unpredictable
+ def _resolve_post(self, file: File, config: MkDocsConfig):
+ post = Post(file, config)
+
+ # Compute path and create a temporary file for path resolution
+ path = self._format_path_for_post(post, config)
+ temp = self._path_to_file(path, config, temp = False)
+
+ # Replace destination file system path and URL
+ file.dest_uri = temp.dest_uri
+ file.abs_dest_path = temp.abs_dest_path
+ file.url = temp.url
+
+ # Replace canonical URL and return post
+ post._set_canonical_url(config.site_url)
+ return post
+
+ # Resolve posts from directory - traverse all documentation pages and filter
+ # and yield those that are located in the posts directory
+ def _resolve_posts(self, files: Files, config: MkDocsConfig):
+ path = self.config.post_dir.format(blog = self.config.blog_dir)
+ path = os.path.normpath(path)
+
+ # Create posts directory, if it does not exist
+ docs = os.path.relpath(config.docs_dir)
+ name = os.path.join(docs, path)
+ if not os.path.isdir(name):
+ os.makedirs(name, exist_ok = True)
+
+ # Filter posts from pages
+ for file in files.documentation_pages():
+ if not file.src_path.startswith(path):
+ continue
+
+ # Temporarily remove post from navigation
+ file.inclusion = InclusionLevel.EXCLUDED
+
+ # Resolve post - in order to determine whether a post should be
+ # excluded, we must load it and analyze its metadata. All posts
+ # marked as drafts are excluded, except for when the author has
+ # configured drafts to be included in the navigation.
+ post = self._resolve_post(file, config)
+ if not self._is_excluded(post):
+ yield post
+
+ # Resolve authors - check if there's an authors file at the configured
+ # location, and if one was found, load and validate it
+ def _resolve_authors(self, config: MkDocsConfig):
+ path = self.config.authors_file.format(blog = self.config.blog_dir)
+ path = os.path.normpath(path)
+
+ # Resolve path relative to docs directory
+ docs = os.path.relpath(config.docs_dir)
+ file = os.path.join(docs, path)
+
+ # If the authors file does not exist, return here
+ config: Authors = Authors()
+ if not os.path.isfile(file):
+ return config.authors
+
+ # Open file and parse as YAML
+ with open(file, encoding = "utf-8") as f:
+ config.config_file_path = os.path.abspath(file)
+ try:
+ config.load_dict(yaml.load(f, SafeLoader) or {})
+
+ # The authors file could not be loaded because of a syntax error,
+ # which we display to the author with a nice error message
+ except Exception as e:
+ raise PluginError(
+ f"Error reading authors file '{path}' in '{docs}':\n"
+ f"{e}"
+ )
+
+ # Validate authors and throw if errors occurred
+ errors, warnings = config.validate()
+ if not config.authors and warnings:
+ log.warning(
+ f"Action required: the format of the authors file changed.\n"
+ f"All authors must now be located under the 'authors' key.\n"
+ f"Please adjust '{file}' to match:\n"
+ f"\n"
+ f"authors:\n"
+ f" squidfunk:\n"
+ f" avatar: https://avatars.githubusercontent.com/u/932156\n"
+ f" description: Creator\n"
+ f" name: Martin Donath\n"
+ f"\n"
+ )
+ for _, w in warnings:
+ log.warning(w)
+ for _, e in errors:
+ raise PluginError(
+ f"Error reading authors file '{path}' in '{docs}':\n"
+ f"{e}"
+ )
+
+ # Return authors
+ return config.authors
+
+ # Resolve views of the given view in pre-order
+ def _resolve_views(self, view: View):
+ yield view
+
+ # Resolve views recursively
+ for page in view.views:
+ for next in self._resolve_views(page):
+ assert isinstance(next, View)
+ yield next
+
+ # Resolve siblings of a navigation item
+ def _resolve_siblings(self, item: StructureItem, nav: Navigation):
+ if isinstance(item.parent, Section):
+ return item.parent.children
+ else:
+ return nav.items
+
+ # Resolve original page or view (e.g. for paginated views)
+ def _resolve_original(self, page: Page):
+ if isinstance(page, View):
+ return page.pages[0]
+ else:
+ return page
+
+ # -------------------------------------------------------------------------
+
+ # Generate views for archive - analyze posts and generate the necessary
+ # views, taking the date format provided by the author into account
+ def _generate_archive(self, config: MkDocsConfig, files: Files):
+ for post in self.blog.posts:
+ date = post.config.date.created
+
+ # Compute name and path of archive view
+ name = self._format_date_for_archive(date, config)
+ path = self._format_path_for_archive(post, config)
+
+ # Create file for view, if it does not exist
+ file = files.get_file_from_path(path)
+ if not file or self.temp_dir not in file.abs_src_path:
+ file = self._path_to_file(path, config)
+ files.append(file)
+
+ # Create file in temporary directory
+ self._save_to_file(file.abs_src_path, f"# {name}")
+
+ # Create and yield view - we don't explicitly set the title of
+ # the view, so authors can override them in the page's content
+ if not isinstance(file.page, Archive):
+ yield Archive(None, file, config)
+
+ # Assign post to archive
+ assert isinstance(file.page, Archive)
+ file.page.posts.append(post)
+
+ # Generate views for categories - analyze posts and generate the necessary
+ # views, taking the allowed categories as set by the author into account
+ def _generate_categories(self, config: MkDocsConfig, files: Files):
+ for post in self.blog.posts:
+ for name in post.config.categories:
+ path = self._format_path_for_category(name)
+
+ # Ensure category is in non-empty allow list
+ categories = self.config.categories_allowed or [name]
+ if name not in categories:
+ docs = os.path.relpath(config.docs_dir)
+ path = os.path.relpath(post.file.abs_src_path, docs)
+ raise PluginError(
+ f"Error reading categories of post '{path}' in "
+ f"'{docs}': category '{name}' not in allow list"
+ )
+
+ # Create file for view, if it does not exist
+ file = files.get_file_from_path(path)
+ if not file or self.temp_dir not in file.abs_src_path:
+ file = self._path_to_file(path, config)
+ files.append(file)
+
+ # Create file in temporary directory
+ self._save_to_file(file.abs_src_path, f"# {name}")
+
+ # Create and yield view - we don't explicitly set the title of
+ # the view, so authors can override them in the page's content
+ if not isinstance(file.page, Category):
+ yield Category(None, file, config)
+
+ # Assign post to category and vice versa
+ assert isinstance(file.page, Category)
+ file.page.posts.append(post)
+ post.categories.append(file.page)
+
+ # Generate pages for pagination - analyze view and generate the necessary
+ # pages, creating a chain of views for simple rendering and replacement
+ def _generate_pages(self, view: View, config: MkDocsConfig, files: Files):
+ yield view
+
+ # Compute pagination boundaries and create pages - pages are internally
+ # handled as copies of a view, as they map to the same source location
+ step = self.config.pagination_per_page
+ for at in range(step, len(view.posts), step):
+ path = self._format_path_for_pagination(view, 1 + at // step)
+
+ # Create file for view, if it does not exist
+ file = files.get_file_from_path(path)
+ if not file or self.temp_dir not in file.abs_src_path:
+ file = self._path_to_file(path, config)
+ files.append(file)
+
+ # Copy file to temporary directory
+ copy_file(view.file.abs_src_path, file.abs_src_path)
+
+ # Create view and attach to previous page
+ if not isinstance(file.page, View):
+ yield View(None, file, config)
+
+ # Assign pages and posts to view
+ assert isinstance(file.page, View)
+ file.page.pages = view.pages
+ file.page.posts = view.posts
+
+ # -------------------------------------------------------------------------
+
+ # Attach a list of pages to each other and to the given parent item without
+ # explicitly adding them to the navigation, which can be done by the caller
+ def _attach(self, parent: StructureItem, pages: list[Page]):
+ for tail, page, head in zip(pages, pages[1:], pages[2:]):
+
+ # Link page to parent and siblings
+ page.parent = parent
+ page.previous_page = tail
+ page.next_page = head
+
+ # If the page is a view, we know that we generated it and need to
+ # link its siblings back to the view
+ if isinstance(page, View):
+ view = self._resolve_original(page)
+ if tail: tail.next_page = view
+ if head: head.previous_page = view
+
+ # Attach a page to the given parent and link it to the previous and next
+ # page of the given host - this is exclusively used for paginated views
+ def _attach_at(self, parent: StructureItem, host: Page, page: Page):
+ self._attach(parent, [host.previous_page, page, host.next_page])
+
+ # Attach a section as a sibling to the given view, make sure its pages are
+ # part of the navigation, and ensure all pages are linked correctly
+ def _attach_to(self, view: View, section: Section, nav: Navigation):
+ section.parent = view.parent
+
+ # Resolve siblings, which are the children of the parent section, or
+ # the top-level list of navigation items if the view is at the root of
+ # the project, and append the given section to it. It's currently not
+ # possible to chose the position of a section.
+ items = self._resolve_siblings(view, nav)
+ items.append(section)
+
+ # Find last sibling that is a page, skipping sections, as we need to
+ # append the given section after all other pages
+ tail = next(item for item in reversed(items) if isinstance(item, Page))
+ head = tail.next_page
+
+ # Attach section to navigation and pages to each other
+ nav.pages.extend(section.children)
+ self._attach(section, [tail, *section.children, head])
+
+ # -------------------------------------------------------------------------
+
+ # Render excerpts and pagination for the given view
+ def _render(self, view: View):
+ posts, pagination = view.posts, None
+
+ # Create pagination, if enabled
+ if self.config.pagination:
+ at = view.pages.index(view)
+
+ # Compute pagination boundaries
+ step = self.config.pagination_per_page
+ p, q = at * step, at * step + step
+
+ # Extract posts in pagination boundaries
+ posts = view.posts[p:q]
+ pagination = self._render_pagination(view, (p, q))
+
+ # Render excerpts for selected posts
+ posts = [
+ self._render_post(post.excerpt, view)
+ for post in posts if post.excerpt
+ ]
+
+ # Return posts and pagination
+ return posts, pagination
+
+ # Render excerpt in the context of the given view
+ def _render_post(self, excerpt: Excerpt, view: View):
+ excerpt.render(view, self.config.post_excerpt_separator)
+
+ # Determine whether to add posts to the table of contents of the view -
+ # note that those settings can be changed individually for each type of
+ # view, which is why we need to check the type of view and the table of
+ # contents setting for that type of view
+ toc = self.config.blog_toc
+ if isinstance(view, Archive):
+ toc = self.config.archive_toc
+ if isinstance(view, Category):
+ toc = self.config.categories_toc
+
+ # Attach top-level table of contents item to view if it should be added
+ # and both, the view and excerpt contain table of contents items
+ if toc and excerpt.toc.items and view.toc.items:
+ view.toc.items[0].children.append(excerpt.toc.items[0])
+
+ # Return excerpt
+ return excerpt
+
+ # Create pagination for the given view and range
+ def _render_pagination(self, view: View, range: tuple[int, int]):
+ p, q = range
+
+ # Create URL from the given page to another page
+ def url_maker(n: int):
+ return get_relative_url(view.pages[n - 1].url, view.url)
+
+ # Return pagination
+ return Pagination(
+ view.posts, page = q // (q - p),
+ items_per_page = q - p,
+ url_maker = url_maker
+ )
+
+ # -------------------------------------------------------------------------
+
+ # Format path for post
+ def _format_path_for_post(self, post: Post, config: MkDocsConfig):
+ categories = post.config.categories[:self.config.post_url_max_categories]
+ categories = [self._slugify_category(name) for name in categories]
+
+ # Replace placeholders in format string
+ date = post.config.date.created
+ path = self.config.post_url_format.format(
+ categories = "/".join(categories),
+ date = self._format_date_for_post_url(date, config),
+ file = post.file.name,
+ slug = post.config.slug or self._slugify_post(post)
+ )
+
+ # Normalize path and strip slashes at the beginning and end
+ path = posixpath.normpath(path.strip("/"))
+ return posixpath.join(self.config.blog_dir, f"{path}.md")
+
+ # Format path for archive
+ def _format_path_for_archive(self, post: Post, config: MkDocsConfig):
+ date = post.config.date.created
+ path = self.config.archive_url_format.format(
+ date = self._format_date_for_archive_url(date, config)
+ )
+
+ # Normalize path and strip slashes at the beginning and end
+ path = posixpath.normpath(path.strip("/"))
+ return posixpath.join(self.config.blog_dir, f"{path}.md")
+
+ # Format path for category
+ def _format_path_for_category(self, name: str):
+ path = self.config.categories_url_format.format(
+ slug = self._slugify_category(name)
+ )
+
+ # Normalize path and strip slashes at the beginning and end
+ path = posixpath.normpath(path.strip("/"))
+ return posixpath.join(self.config.blog_dir, f"{path}.md")
+
+ # Format path for pagination
+ def _format_path_for_pagination(self, view: View, page: int):
+ path = self.config.pagination_url_format.format(
+ page = page
+ )
+
+ # Compute base path for pagination - if the given view is an index file,
+ # we need to pop the file name from the base so it's not part of the URL
+ # and we need to append `index` to the path, so the paginated view is
+ # also an index page - see https://t.ly/71MKF
+ base, _ = posixpath.splitext(view.file.src_uri)
+ if view.is_index:
+ base = posixpath.dirname(base)
+ path = posixpath.join(path, "index")
+
+ # Normalize path and strip slashes at the beginning and end
+ path = posixpath.normpath(path.strip("/"))
+ return posixpath.join(base, f"{path}.md")
+
+ # -------------------------------------------------------------------------
+
+ # Format date
+ def _format_date(self, date: datetime, format: str, config: MkDocsConfig):
+ locale = config.theme["language"]
+ return format_date(date, format = format, locale = locale)
+
+ # Format date for post
+ def _format_date_for_post(self, date: datetime, config: MkDocsConfig):
+ format = self.config.post_date_format
+ return self._format_date(date, format, config)
+
+ # Format date for post URL
+ def _format_date_for_post_url(self, date: datetime, config: MkDocsConfig):
+ format = self.config.post_url_date_format
+ return self._format_date(date, format, config)
+
+ # Format date for archive
+ def _format_date_for_archive(self, date: datetime, config: MkDocsConfig):
+ format = self.config.archive_date_format
+ return self._format_date(date, format, config)
+
+ # Format date for archive URL
+ def _format_date_for_archive_url(self, date: datetime, config: MkDocsConfig):
+ format = self.config.archive_url_date_format
+ return self._format_date(date, format, config)
+
+ # -------------------------------------------------------------------------
+
+ # Slugify post title
+ def _slugify_post(self, post: Post):
+ separator = self.config.post_slugify_separator
+ return self.config.post_slugify(post.title, separator)
+
+ # Slugify category
+ def _slugify_category(self, name: str):
+ separator = self.config.categories_slugify_separator
+ return self.config.categories_slugify(name, separator)
+
+ # -------------------------------------------------------------------------
+
+ # Create a file for the given path, which must point to a valid source file,
+ # either inside the temporary directory or the docs directory
+ def _path_to_file(self, path: str, config: MkDocsConfig, *, temp = True):
+ assert path.endswith(".md")
+ file = File(
+ path,
+ config.docs_dir if not temp else self.temp_dir,
+ config.site_dir,
+ config.use_directory_urls
+ )
+
+ # Hack: mark file as generated, so other plugins don't think it's part
+ # of the file system. This is more or less a new quasi-standard that
+ # still needs to be adopted by MkDocs, and was introduced by the
+ # git-revision-date-localized-plugin - see https://bit.ly/3ZUmdBx
+ if temp:
+ file.generated_by = "material/blog"
+
+ # Return file
+ return file
+
+ # Create a file with the given content on disk
+ def _save_to_file(self, path: str, content: str):
+ os.makedirs(os.path.dirname(path), exist_ok = True)
+ with open(path, "w", encoding = "utf-8") as f:
+ f.write(content)
+
+ # -------------------------------------------------------------------------
+
+ # Translate the placeholder referenced by the given key
+ def _translate(self, key: str, config: MkDocsConfig) -> str:
+ env = config.theme.get_env()
+ template = env.get_template(
+ "partials/language.html", globals = { "config": config }
+ )
+
+ # Translate placeholder
+ return template.module.t(key)
+
+# -----------------------------------------------------------------------------
+# Data
+# -----------------------------------------------------------------------------
+
+# Set up logging
+log = logging.getLogger("mkdocs.material.blog")
diff --git a/src/plugins/blog/readtime/__init__.py b/src/plugins/blog/readtime/__init__.py
new file mode 100644
index 00000000..a0c149b9
--- /dev/null
+++ b/src/plugins/blog/readtime/__init__.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import re
+
+from math import ceil
+
+from .parser import ReadtimeParser
+
+# -----------------------------------------------------------------------------
+# Functions
+# -----------------------------------------------------------------------------
+
+# Compute readtime - we first used the original readtime library, but the list
+# of dependencies it brings with it increased the size of the Docker image by
+# 20 MB (packed), which is an increase of 50%. For this reason, we adapt the
+# original readtime algorithm to our needs - see https://t.ly/fPZ7L
+def readtime(html: str, words_per_minute: int):
+ parser = ReadtimeParser()
+ parser.feed(html)
+ parser.close()
+
+ # Extract words from text and compute readtime in seconds
+ words = len(re.split(r"\W+", "".join(parser.text)))
+ seconds = ceil(words / words_per_minute * 60)
+
+ # Account for additional images
+ delta = 12
+ for _ in range(parser.images):
+ seconds += delta
+ if delta > 3: delta -= 1
+
+ # Return readtime in minutes
+ return ceil(seconds / 60)
diff --git a/src/plugins/blog/readtime/parser.py b/src/plugins/blog/readtime/parser.py
new file mode 100644
index 00000000..b91a7b30
--- /dev/null
+++ b/src/plugins/blog/readtime/parser.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from html.parser import HTMLParser
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Readtime parser
+class ReadtimeParser(HTMLParser):
+
+ # Initialize parser
+ def __init__(self):
+ super().__init__(convert_charrefs = True)
+
+ # Keep track of text and images
+ self.text = []
+ self.images = 0
+
+ # Collect images
+ def handle_starttag(self, tag, attrs):
+ if tag == "img":
+ self.images += 1
+
+ # Collect text
+ def handle_data(self, data):
+ self.text.append(data)
diff --git a/src/plugins/blog/structure/__init__.py b/src/plugins/blog/structure/__init__.py
new file mode 100644
index 00000000..2fc541fe
--- /dev/null
+++ b/src/plugins/blog/structure/__init__.py
@@ -0,0 +1,292 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from __future__ import annotations
+
+import logging
+import os
+import yaml
+
+from copy import copy
+from markdown import Markdown
+from material.plugins.blog.author import Author
+from mkdocs.config.defaults import MkDocsConfig
+from mkdocs.exceptions import PluginError
+from mkdocs.structure.files import File, Files
+from mkdocs.structure.nav import Section
+from mkdocs.structure.pages import Page, _RelativePathTreeprocessor
+from mkdocs.structure.toc import get_toc
+from mkdocs.utils.meta import YAML_RE
+from re import Match
+from yaml import SafeLoader
+
+from .config import PostConfig
+from .markdown import ExcerptTreeprocessor
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Post
+class Post(Page):
+
+ # Initialize post - posts are never listed in the navigation, which is why
+ # they will never include a title that was manually set, so we can omit it
+ def __init__(self, file: File, config: MkDocsConfig):
+ super().__init__(None, file, config)
+
+ # Resolve path relative to docs directory
+ docs = os.path.relpath(config.docs_dir)
+ path = os.path.relpath(file.abs_src_path, docs)
+
+ # Read contents and metadata immediately
+ with open(file.abs_src_path, encoding = "utf-8") as f:
+ self.markdown = f.read()
+
+ # Sadly, MkDocs swallows any exceptions that occur during parsing.
+ # As we want to provide the best possible authoring experience, we
+ # need to catch errors early and display them nicely. We decided to
+ # drop support for MkDocs' MultiMarkdown syntax, because it is not
+ # correctly implemented anyway. When using MultiMarkdown syntax, all
+ # date formats are returned as strings and list are not properly
+ # supported. Thus, we just use the relevants parts of `get_data`.
+ match: Match = YAML_RE.match(self.markdown)
+ if not match:
+ raise PluginError(
+ f"Error reading metadata of post '{path}' in '{docs}':\n"
+ f"Expected metadata to be defined but found nothing"
+ )
+
+ # Extract metadata and parse as YAML
+ try:
+ self.meta = yaml.load(match.group(1), SafeLoader) or {}
+ self.markdown = self.markdown[match.end():].lstrip("\n")
+
+ # The post's metadata could not be parsed because of a syntax error,
+ # which we display to the user with a nice error message
+ except Exception as e:
+ raise PluginError(
+ f"Error reading metadata of post '{path}' in '{docs}':\n"
+ f"{e}"
+ )
+
+ # Initialize post configuration, but remove all keys that this plugin
+ # doesn't care about, or they will be reported as invalid configuration
+ self.config: PostConfig = PostConfig(file.abs_src_path)
+ self.config.load_dict({
+ key: self.meta[key] for key in (
+ set(self.meta.keys()) &
+ set(self.config.keys())
+ )
+ })
+
+ # Validate configuration and throw if errors occurred
+ errors, warnings = self.config.validate()
+ for _, w in warnings:
+ log.warning(w)
+ for k, e in errors:
+ raise PluginError(
+ f"Error reading metadata '{k}' of post '{path}' in '{docs}':\n"
+ f"{e}"
+ )
+
+ # Excerpts are subsets of posts that are used in pages like archive and
+ # category views. They are not rendered as standalone pages, but are
+ # rendered in the context of a view. Each post has a dedicated excerpt
+ # instance which is reused when rendering views.
+ self.excerpt: Excerpt = None
+
+ # Initialize authors and actegories
+ self.authors: list[Author] = []
+ self.categories: list[Category] = []
+
+ # Ensure template is set or use default
+ self.meta.setdefault("template", "blog-post.html")
+
+ # Ensure template hides navigation
+ self.meta["hide"] = self.meta.get("hide", [])
+ if "navigation" not in self.meta["hide"]:
+ self.meta["hide"].append("navigation")
+
+ # The contents and metadata were already read in the constructor (and not
+ # in `read_source` as for pages), so this function must be set to a no-op
+ def read_source(self, config: MkDocsConfig):
+ pass
+
+# -----------------------------------------------------------------------------
+
+# Excerpt
+class Excerpt(Page):
+
+ # Initialize an excerpt for the given post - we create the Markdown parser
+ # when intitializing the excerpt in order to improve rendering performance
+ # for excerpts, as they are reused across several different views, because
+ # posts might be referenced from multiple different locations
+ def __init__(self, post: Post, config: MkDocsConfig, files: Files):
+ self.file = copy(post.file)
+ self.post = post
+
+ # Set canonical URL, or we can't print excerpts when debugging the
+ # blog plugin, as the `abs_url` property would be missing
+ self._set_canonical_url(config.site_url)
+
+ # Initialize configuration and metadata
+ self.config = post.config
+ self.meta = post.meta
+
+ # Initialize authors and categories - note that views usually contain
+ # subsets of those lists, which is why we need to manage them here
+ self.authors: list[Author] = []
+ self.categories: list[Category] = []
+
+ # Initialize parser - note that we need to patch the configuration,
+ # more specifically the table of contents extension
+ config = _patch(config)
+ self.md = Markdown(
+ extensions = config.markdown_extensions,
+ extension_configs = config.mdx_configs,
+ )
+
+ # Register excerpt tree processor - this processor resolves anchors to
+ # posts from within views, so they point to the correct location
+ self.md.treeprocessors.register(
+ ExcerptTreeprocessor(post),
+ "excerpt",
+ 0
+ )
+
+ # Register relative path tree processor - this processor resolves links
+ # to other pages and assets, and is used by MkDocs itself
+ self.md.treeprocessors.register(
+ _RelativePathTreeprocessor(self.file, files, config),
+ "relpath",
+ 1
+ )
+
+ # Render an excerpt of the post on the given page - note that this is not
+ # thread-safe because excerpts are shared across views, as it cuts down on
+ # the cost of initialization. However, if in the future, we decide to render
+ # posts and views concurrently, we must change this behavior.
+ def render(self, page: Page, separator: str):
+ self.file.url = page.url
+
+ # Retrieve excerpt tree processor and set page as base
+ at = self.md.treeprocessors.get_index_for_name("excerpt")
+ processor: ExcerptTreeprocessor = self.md.treeprocessors[at]
+ processor.base = page
+
+ # Ensure that the excerpt includes a title in its content, since the
+ # title is linked to the post when rendering - see https://t.ly/5Gg2F
+ self.markdown = self.post.markdown
+ if not self.post._title_from_render:
+ self.markdown = "\n\n".join([f"# {self.post.title}", self.markdown])
+
+ # Convert Markdown to HTML and extract excerpt
+ self.content = self.md.convert(self.markdown)
+ self.content, *_ = self.content.split(separator, 1)
+
+ # Extract table of contents and reset post URL - if we wouldn't reset
+ # the excerpt URL, linking to the excerpt from the view would not work
+ self.toc = get_toc(getattr(self.md, "toc_tokens", []))
+ self.file.url = self.post.url
+
+# -----------------------------------------------------------------------------
+
+# View
+class View(Page):
+
+ # Initialize view
+ def __init__(self, title: str | None, file: File, config: MkDocsConfig):
+ super().__init__(title, file, config)
+ self.parent: View | Section
+
+ # Initialize posts and views
+ self.posts: list[Post] = []
+ self.views: list[View] = []
+
+ # Initialize pages for pagination
+ self.pages: list[View] = []
+
+ # Set necessary metadata
+ def read_source(self, config: MkDocsConfig):
+ super().read_source(config)
+
+ # Ensure template is set or use default
+ self.meta.setdefault("template", "blog.html")
+
+# -----------------------------------------------------------------------------
+
+# Archive view
+class Archive(View):
+ pass
+
+# -----------------------------------------------------------------------------
+
+# Category view
+class Category(View):
+ pass
+
+# -----------------------------------------------------------------------------
+# Helper functions
+# -----------------------------------------------------------------------------
+
+# Patch configuration
+def _patch(config: MkDocsConfig):
+ config = copy(config)
+
+ # Copy parts of configuration that needs to be patched
+ config.validation = copy(config.validation)
+ config.validation.links = copy(config.validation.links)
+ config.markdown_extensions = copy(config.markdown_extensions)
+ config.mdx_configs = copy(config.mdx_configs)
+
+ # Make sure that the author did not add another instance of the table of
+ # contents extension to the configuration, as this leads to weird behavior
+ if "markdown.extensions.toc" in config.markdown_extensions:
+ config.markdown_extensions.remove("markdown.extensions.toc")
+
+ # In order to render excerpts for posts, we need to make sure that the
+ # table of contents extension is appropriately configured
+ config.mdx_configs["toc"] = {
+ **config.mdx_configs.get("toc", {}),
+ **{
+ "anchorlink": True, # Render headline as clickable
+ "baselevel": 2, # Render h1 as h2 and so forth
+ "permalink": False, # Remove permalinks
+ "toc_depth": 2 # Remove everything below h2
+ }
+ }
+
+ # Additionally, we disable link validation when rendering excerpts, because
+ # invalid links have already been reported when rendering the page
+ links = config.validation.links
+ links.not_found = logging.DEBUG
+ links.absolute_links = logging.DEBUG
+ links.unrecognized_links = logging.DEBUG
+
+ # Return patched configuration
+ return config
+
+# -----------------------------------------------------------------------------
+# Data
+# -----------------------------------------------------------------------------
+
+# Set up logging
+log = logging.getLogger("mkdocs.material.blog")
diff --git a/src/plugins/blog/structure/config.py b/src/plugins/blog/structure/config.py
new file mode 100644
index 00000000..129491b9
--- /dev/null
+++ b/src/plugins/blog/structure/config.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from mkdocs.config.base import Config
+from mkdocs.config.config_options import ListOfItems, Optional, Type
+
+from .options import PostDate
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Post configuration
+class PostConfig(Config):
+ authors = ListOfItems(Type(str), default = [])
+ categories = ListOfItems(Type(str), default = [])
+ date = PostDate()
+ draft = Optional(Type(bool))
+ readtime = Optional(Type(int))
+ slug = Optional(Type(str))
diff --git a/src/plugins/blog/structure/markdown.py b/src/plugins/blog/structure/markdown.py
new file mode 100644
index 00000000..64ade554
--- /dev/null
+++ b/src/plugins/blog/structure/markdown.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from markdown.treeprocessors import Treeprocessor
+from mkdocs.structure.pages import Page
+from mkdocs.utils import get_relative_url
+from xml.etree.ElementTree import Element
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Excerpt tree processor
+class ExcerptTreeprocessor(Treeprocessor):
+
+ # Initialize excerpt tree processor
+ def __init__(self, page: Page, base: Page = None):
+ self.page = page
+ self.base = base
+
+ # Transform HTML after Markdown processing
+ def run(self, root: Element):
+ main = True
+
+ # We're only interested in anchors, which is why we continue when the
+ # link does not start with an anchor tag
+ for el in root.iter("a"):
+ anchor = el.get("href")
+ if not anchor.startswith("#"):
+ continue
+
+ # The main headline should link to the post page, not to a specific
+ # anchor, which is why we remove the anchor in that case
+ path = get_relative_url(self.page.url, self.base.url)
+ if main:
+ el.set("href", path)
+ else:
+ el.set("href", path + anchor)
+
+ # Main headline has been seen
+ main = False
diff --git a/src/plugins/blog/structure/options.py b/src/plugins/blog/structure/options.py
new file mode 100644
index 00000000..281dec9f
--- /dev/null
+++ b/src/plugins/blog/structure/options.py
@@ -0,0 +1,87 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from datetime import date, datetime, time
+from mkdocs.config.base import BaseConfigOption, Config, ValidationError
+from typing import Dict
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Date dictionary
+class DateDict(Dict[str, datetime]):
+
+ # Initialize date dictionary
+ def __init__(self, data: dict):
+ super().__init__(data)
+
+ # Ensure presence of `date.created`
+ self.created: datetime = data["created"]
+
+ # Allow attribute access
+ def __getattr__(self, name: str):
+ if name in self:
+ return self[name]
+
+# -----------------------------------------------------------------------------
+
+# Post date option
+class PostDate(BaseConfigOption[DateDict]):
+
+ # Initialize post dates
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Normalize the supported types for post dates to datetime
+ def pre_validation(self, config: Config, key_name: str):
+
+ # If the date points to a scalar value, convert it to a dictionary,
+ # since we want to allow the user to specify custom and arbitrary date
+ # values for posts. Currently, only the `created` date is mandatory,
+ # because it's needed to sort posts for views.
+ if not isinstance(config[key_name], dict):
+ config[key_name] = { "created": config[key_name] }
+
+ # Convert all date values to datetime
+ for key, value in config[key_name].items():
+ if isinstance(value, date):
+ config[key_name][key] = datetime.combine(value, time())
+
+ # Initialize date dictionary
+ config[key_name] = DateDict(config[key_name])
+
+ # Ensure each date value is of type datetime
+ def run_validation(self, value: DateDict):
+ for key in value:
+ if not isinstance(value[key], datetime):
+ raise ValidationError(
+ f"Expected type: {date} or {datetime} "
+ f"but received: {type(value[key])}"
+ )
+
+ # Ensure presence of `date.created`
+ if not value.created:
+ raise ValidationError(
+ "Expected 'created' date when using dictionary syntax"
+ )
+
+ # Return date dictionary
+ return value
diff --git a/src/plugins/blog/templates/__init__.py b/src/plugins/blog/templates/__init__.py
new file mode 100644
index 00000000..9f7d794b
--- /dev/null
+++ b/src/plugins/blog/templates/__init__.py
@@ -0,0 +1,42 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from jinja2 import pass_context
+from jinja2.runtime import Context
+from material.plugins.blog.structure import View
+from mkdocs.utils.templates import url_filter as _url_filter
+
+# -----------------------------------------------------------------------------
+# Functions
+# -----------------------------------------------------------------------------
+
+# Filter for normalizing URLs with support for paginated views
+@pass_context
+def url_filter(context: Context, url: str):
+ page = context["page"]
+
+ # If the current page is a view, check if the URL links to the page
+ # itself, and replace it with the URL of the main view
+ if isinstance(page, View):
+ if page.url == url:
+ url = page.pages[0].url
+
+ # Forward to original template filter
+ return _url_filter(context, url)
diff --git a/src/plugins/group/__init__.py b/src/plugins/group/__init__.py
new file mode 100644
index 00000000..d1899378
--- /dev/null
+++ b/src/plugins/group/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
diff --git a/src/plugins/group/config.py b/src/plugins/group/config.py
new file mode 100644
index 00000000..fb19222a
--- /dev/null
+++ b/src/plugins/group/config.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from __future__ import annotations
+
+from mkdocs.config.config_options import Type
+from mkdocs.config.base import Config
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Group plugin configuration
+class GroupConfig(Config):
+ enabled = Type(bool, default = False)
+ plugins = Type(list | dict)
diff --git a/src/plugins/group/plugin.py b/src/plugins/group/plugin.py
new file mode 100644
index 00000000..4ab13dbf
--- /dev/null
+++ b/src/plugins/group/plugin.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import logging
+
+from collections.abc import Callable
+from mkdocs.config.config_options import Plugins
+from mkdocs.config.defaults import MkDocsConfig
+from mkdocs.exceptions import PluginError
+from mkdocs.plugins import BasePlugin, event_priority
+
+from .config import GroupConfig
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Group plugin
+class GroupPlugin(BasePlugin[GroupConfig]):
+ supports_multiple_instances = True
+
+ # Determine whether we're serving the site
+ def on_startup(self, *, command, dirty):
+ self.is_serve = command == "serve"
+ self.is_dirty = dirty
+
+ # If the group is enabled, conditionally load plugins - at first, this might
+ # sound easier than it actually is, as we need to jump through some hoops to
+ # ensure correct ordering among plugins. We're effectively initializing the
+ # plugins that are part of the group after all MkDocs finished initializing
+ # all other plugins, so we need to patch the order of the methods. Moreover,
+ # we must use MkDocs existing plugin collection, or we might have collisions
+ # with other plugins that are not part of the group. As so often, this is a
+ # little hacky, but has huge potential making plugin configuration easier.
+ # There's one little caveat: the `__init__` and `on_startup` methods of the
+ # plugins that are part of the group are called after all other plugins, so
+ # the `event_priority` decorator for `on_startup` events and is effectively
+ # useless. However, the `on_startup` event is only intended to set up the
+ # plugin and doesn't receive anything else than the invoked command and
+ # whether we're running a dirty build, so there should be no problems.
+ @event_priority(150)
+ def on_config(self, config):
+ if not self.config.enabled:
+ return
+
+ # Retrieve plugin collection from configuration
+ option: Plugins = dict(config._schema)["plugins"]
+ assert isinstance(option, Plugins)
+
+ # Load all plugins in group
+ self.plugins: dict[str, BasePlugin] = {}
+ try:
+ for name, plugin in self._load(option):
+ self.plugins[name] = plugin
+
+ # The plugin could not be loaded, likely because it's not installed or
+ # misconfigured, so we raise a plugin error for a nicer error message
+ except Exception as e:
+ raise PluginError(str(e))
+
+ # Patch order of plugin methods
+ for events in option.plugins.events.values():
+ self._patch(events, config)
+
+ # Invoke `on_startup` event for plugins in group
+ command = "serve" if self.is_serve else "build"
+ for method in option.plugins.events["startup"]:
+ plugin = self._get_plugin(method)
+
+ # Ensure that we have a method bound to a plugin (and not a hook)
+ if plugin and plugin in self.plugins.values():
+ method(command = command, dirty = self.is_dirty)
+
+ # -------------------------------------------------------------------------
+
+ # Retrieve plugin instance for bound method or nothing
+ def _get_plugin(self, method: Callable):
+ return getattr(method, "__self__", None)
+
+ # Retrieve priority of plugin method
+ def _get_priority(self, method: Callable):
+ return getattr(method, "mkdocs_priority", 0)
+
+ # Retrieve position of plugin
+ def _get_position(self, plugin: BasePlugin, config: MkDocsConfig) -> int:
+ for at, (_, candidate) in enumerate(config.plugins.items()):
+ if plugin == candidate:
+ return at
+
+ # -------------------------------------------------------------------------
+
+ # Load plugins that are part of the group
+ def _load(self, option: Plugins):
+ for name, data in option._parse_configs(self.config.plugins):
+ yield option.load_plugin_with_namespace(name, data)
+
+ # -------------------------------------------------------------------------
+
+ # Patch order of plugin methods - all other plugin methods are already in
+ # the right order, so we only need to check those that are part of the group
+ # and bubble them up into the right location. Some plugin methods may define
+ # priorities, so we need to make sure to order correctly within those.
+ def _patch(self, methods: list[Callable], config: MkDocsConfig):
+ position = self._get_position(self, config)
+ for at in reversed(range(1, len(methods))):
+ tail = methods[at - 1]
+ head = methods[at]
+
+ # Skip if the plugin is not part of the group
+ plugin = self._get_plugin(head)
+ if not plugin or plugin not in self.plugins.values():
+ continue
+
+ # Skip if the previous method has a higher priority than the current
+ # one, because we know we can't swap them anyway
+ if self._get_priority(tail) > self._get_priority(head):
+ continue
+
+ # Ensure that we have a method bound to a plugin (and not a hook)
+ plugin = self._get_plugin(tail)
+ if not plugin:
+ continue
+
+ # Both methods have the same priority, so we check if the ordering
+ # of both methods is violated, and if it is, swap them
+ if (position < self._get_position(plugin, config)):
+ methods[at], methods[at - 1] = tail, head
+
+# -----------------------------------------------------------------------------
+# Data
+# -----------------------------------------------------------------------------
+
+# Set up logging
+log = logging.getLogger("mkdocs.material.group")
diff --git a/src/plugins/info/__init__.py b/src/plugins/info/__init__.py
new file mode 100644
index 00000000..d1899378
--- /dev/null
+++ b/src/plugins/info/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
diff --git a/src/plugins/info/config.py b/src/plugins/info/config.py
new file mode 100644
index 00000000..cbd64d4c
--- /dev/null
+++ b/src/plugins/info/config.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from mkdocs.config.config_options import Type
+from mkdocs.config.base import Config
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Info plugin configuration
+class InfoConfig(Config):
+ enabled = Type(bool, default = True)
+ enabled_on_serve = Type(bool, default = False)
+
+ # Settings for archive
+ archive = Type(bool, default = True)
+ archive_stop_on_violation = Type(bool, default = True)
diff --git a/src/plugins/info/plugin.py b/src/plugins/info/plugin.py
new file mode 100644
index 00000000..7c6fdc17
--- /dev/null
+++ b/src/plugins/info/plugin.py
@@ -0,0 +1,245 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import json
+import logging
+import os
+import platform
+import requests
+import sys
+
+from colorama import Fore, Style
+from importlib.metadata import distributions, version
+from io import BytesIO
+from markdown.extensions.toc import slugify
+from mkdocs.plugins import BasePlugin, event_priority
+from mkdocs.structure.files import get_files
+from mkdocs.utils import get_theme_dir
+from zipfile import ZipFile, ZIP_DEFLATED
+
+from .config import InfoConfig
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Info plugin
+class InfoPlugin(BasePlugin[InfoConfig]):
+
+ # Initialize plugin
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Initialize incremental builds
+ self.is_serve = False
+
+ # Determine whether we're serving the site
+ def on_startup(self, *, command, dirty):
+ self.is_serve = command == "serve"
+
+ # Create a self-contained example (run earliest) - determine all files that
+ # are visible to MkDocs and are used to build the site, create an archive
+ # that contains all of them, and print a summary of the archive contents.
+ # The user must attach this archive to the bug report.
+ @event_priority(100)
+ def on_config(self, config):
+ if not self.config.enabled:
+ return
+
+ # By default, the plugin is disabled when the documentation is served,
+ # but not when it is built. This should nicely align with the expected
+ # user experience when creating reproductions.
+ if not self.config.enabled_on_serve and self.is_serve:
+ return
+
+ # Resolve latest version
+ url = "https://github.com/squidfunk/mkdocs-material/releases/latest"
+ res = requests.get(url, allow_redirects = False)
+
+ # Check if we're running the latest version
+ _, current = res.headers.get("location").rsplit("/", 1)
+ present = version("mkdocs-material")
+ if not present.startswith(current):
+ log.error("Please upgrade to the latest version.")
+ self._help_on_versions_and_exit(present, current)
+
+ # Exit if archive creation is disabled
+ if not self.config.archive:
+ sys.exit(1)
+
+ # Print message that we're creating a bug report
+ log.info("Started archive creation for bug report")
+
+ # Check that there are no overrides in place - we need to use a little
+ # hack to detect whether the custom_dir setting was used without parsing
+ # mkdocs.yml again - we check at which position the directory provided
+ # by the theme resides, and if it's not the first one, abort.
+ if config.theme.dirs.index(get_theme_dir(config.theme.name)):
+ log.error("Please remove 'custom_dir' setting.")
+ self._help_on_customizations_and_exit()
+
+ # Check that there are no hooks in place - hooks can alter the behavior
+ # of MkDocs in unpredictable ways, which is why they must be considered
+ # being customizations. Thus, we can't offer support for debugging and
+ # must abort here.
+ if config.hooks:
+ log.error("Please remove 'hooks' setting.")
+ self._help_on_customizations_and_exit()
+
+ # Create in-memory archive and prompt user to enter a short descriptive
+ # name for the archive, which is also used as the directory name. Note
+ # that the name is slugified for better readability and stripped of any
+ # file extension that the user might have entered.
+ archive = BytesIO()
+ example = input("\nPlease name your bug report (2-4 words): ")
+ example, _ = os.path.splitext(example)
+ example = "-".join([present, slugify(example, "-")])
+
+ # Create self-contained example from project
+ files: list[str] = []
+ with ZipFile(archive, "a", ZIP_DEFLATED, False) as f:
+ for path in ["mkdocs.yml", "requirements.txt"]:
+ if os.path.isfile(path):
+ f.write(path, os.path.join(example, path))
+
+ # Append all files visible to MkDocs
+ for file in get_files(config):
+ path = os.path.relpath(file.abs_src_path, os.path.curdir)
+ f.write(path, os.path.join(example, path))
+
+ # Add information on installed packages
+ f.writestr(
+ os.path.join(example, "requirements.lock.txt"),
+ "\n".join(sorted([
+ "==".join([package.name, package.version])
+ for package in distributions()
+ ]))
+ )
+
+ # Add information on platform
+ f.writestr(
+ os.path.join(example, "platform.json"),
+ json.dumps(
+ {
+ "system": platform.platform(),
+ "python": platform.python_version()
+ },
+ default = str,
+ indent = 2
+ )
+ )
+
+ # Retrieve list of processed files
+ for a in f.filelist:
+ files.append("".join([
+ Fore.LIGHTBLACK_EX, a.filename, " ",
+ _size(a.compress_size)
+ ]))
+
+ # Finally, write archive to disk
+ buffer = archive.getbuffer()
+ with open(f"{example}.zip", "wb") as f:
+ f.write(archive.getvalue())
+
+ # Print summary
+ log.info("Archive successfully created:")
+ print(Style.NORMAL)
+
+ # Print archive file names
+ files.sort()
+ for file in files:
+ print(f" {file}")
+
+ # Print archive name
+ print(Style.RESET_ALL)
+ print("".join([
+ " ", f.name, " ",
+ _size(buffer.nbytes, 10)
+ ]))
+
+ # Print warning when file size is excessively large
+ print(Style.RESET_ALL)
+ if buffer.nbytes > 1000000:
+ log.warning("Archive exceeds recommended maximum size of 1 MB")
+
+ # Aaaaaand done
+ sys.exit(1)
+
+ # -------------------------------------------------------------------------
+
+ # Print help on versions and exit
+ def _help_on_versions_and_exit(self, have, need):
+ print(Fore.RED)
+ print(" When reporting issues, please first upgrade to the latest")
+ print(" version of Material for MkDocs, as the problem might already")
+ print(" be fixed in the latest version. This helps reduce duplicate")
+ print(" efforts and saves us maintainers time.")
+ print(Style.NORMAL)
+ print(f" Please update from {have} to {need}.")
+ print(Style.RESET_ALL)
+ print(f" pip install --upgrade --force-reinstall mkdocs-material")
+ print(Style.NORMAL)
+
+ # Exit, unless explicitly told not to
+ if self.config.archive_stop_on_violation:
+ sys.exit(1)
+
+ # Print help on customizations and exit
+ def _help_on_customizations_and_exit(self):
+ print(Fore.RED)
+ print(" When reporting issues, you must remove all customizations")
+ print(" and check if the problem persists. If not, the problem is")
+ print(" caused by your overrides. Please understand that we can't")
+ print(" help you debug your customizations. Please remove:")
+ print(Style.NORMAL)
+ print(" - theme.custom_dir")
+ print(" - hooks")
+ print(Fore.YELLOW)
+ print(" Additionally, please remove all third-party JavaScript or")
+ print(" CSS not explicitly mentioned in our documentation:")
+ print(Style.NORMAL)
+ print(" - extra_css")
+ print(" - extra_javascript")
+ print(Style.RESET_ALL)
+
+ # Exit, unless explicitly told not to
+ if self.config.archive_stop_on_violation:
+ sys.exit(1)
+
+# -----------------------------------------------------------------------------
+# Helper functions
+# -----------------------------------------------------------------------------
+
+# Print human-readable size
+def _size(value, factor = 1):
+ color = Fore.GREEN
+ if value > 100000 * factor: color = Fore.RED
+ elif value > 25000 * factor: color = Fore.YELLOW
+ for unit in ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB"]:
+ if abs(value) < 1000.0:
+ return f"{color}{value:3.1f} {unit}"
+ value /= 1000.0
+
+# -----------------------------------------------------------------------------
+# Data
+# -----------------------------------------------------------------------------
+
+# Set up logging
+log = logging.getLogger("mkdocs.material.info")
diff --git a/src/plugins/offline/__init__.py b/src/plugins/offline/__init__.py
new file mode 100644
index 00000000..d1899378
--- /dev/null
+++ b/src/plugins/offline/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
diff --git a/src/plugins/offline/config.py b/src/plugins/offline/config.py
new file mode 100644
index 00000000..49f51a94
--- /dev/null
+++ b/src/plugins/offline/config.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from mkdocs.config.config_options import Type
+from mkdocs.config.base import Config
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Offline plugin configuration
+class OfflineConfig(Config):
+ enabled = Type(bool, default = True)
diff --git a/src/plugins/offline/plugin.py b/src/plugins/offline/plugin.py
new file mode 100644
index 00000000..abcb2598
--- /dev/null
+++ b/src/plugins/offline/plugin.py
@@ -0,0 +1,69 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import os
+
+from mkdocs.plugins import BasePlugin, event_priority
+
+from .config import OfflineConfig
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Offline plugin
+class OfflinePlugin(BasePlugin[OfflineConfig]):
+
+ # Set configuration for offline build
+ def on_config(self, config):
+ if not self.config.enabled:
+ return
+
+ # Ensure correct resolution of links when viewing the site from the
+ # file system by disabling directory URLs
+ config.use_directory_urls = False
+
+ # Append iframe-worker to polyfills/shims
+ config.extra["polyfills"] = config.extra.get("polyfills", [])
+ if not any("iframe-worker" in url for url in config.extra["polyfills"]):
+ script = "https://unpkg.com/iframe-worker/shim"
+ config.extra["polyfills"].append(script)
+
+ # Add support for offline search (run latest) - the search index is copied
+ # and inlined into a script, so that it can be used without a server
+ @event_priority(-100)
+ def on_post_build(self, *, config):
+ if not self.config.enabled:
+ return
+
+ # Ensure presence of search index
+ path = os.path.join(config.site_dir, "search")
+ file = os.path.join(path, "search_index.json")
+ if not os.path.isfile(file):
+ return
+
+ # Obtain search index contents
+ with open(file, encoding = "utf-8") as f:
+ data = f.read()
+
+ # Inline search index contents into script
+ file = os.path.join(path, "search_index.js")
+ with open(file, "w", encoding = "utf-8") as f:
+ f.write(f"var __index = {data}")
diff --git a/src/plugins/search/__init__.py b/src/plugins/search/__init__.py
new file mode 100644
index 00000000..d1899378
--- /dev/null
+++ b/src/plugins/search/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
diff --git a/src/plugins/search/config.py b/src/plugins/search/config.py
new file mode 100644
index 00000000..e150fbb3
--- /dev/null
+++ b/src/plugins/search/config.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from mkdocs.config.config_options import (
+ Choice,
+ Deprecated,
+ Optional,
+ ListOfItems,
+ Type
+)
+from mkdocs.config.base import Config
+from mkdocs.contrib.search import LangOption
+
+# -----------------------------------------------------------------------------
+# Options
+# -----------------------------------------------------------------------------
+
+# Options for search pipeline
+pipeline = ("stemmer", "stopWordFilter", "trimmer")
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Search plugin configuration
+class SearchConfig(Config):
+ enabled = Type(bool, default = True)
+
+ # Settings for search
+ lang = Optional(LangOption())
+ separator = Optional(Type(str))
+ pipeline = ListOfItems(Choice(pipeline), default = [])
+
+ # Settings for text segmentation (Chinese)
+ jieba_dict = Optional(Type(str))
+ jieba_dict_user = Optional(Type(str))
+
+ # Unsupported settings, originally implemented in MkDocs
+ indexing = Deprecated(message = "Unsupported option")
+ prebuild_index = Deprecated(message = "Unsupported option")
+ min_search_length = Deprecated(message = "Unsupported option")
diff --git a/src/plugins/search/plugin.py b/src/plugins/search/plugin.py
new file mode 100644
index 00000000..5c254e3f
--- /dev/null
+++ b/src/plugins/search/plugin.py
@@ -0,0 +1,580 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import json
+import logging
+import os
+import regex as re
+
+from html import escape
+from html.parser import HTMLParser
+from mkdocs import utils
+from mkdocs.plugins import BasePlugin
+
+from .config import SearchConfig
+
+try:
+ import jieba
+except ImportError:
+ jieba = None
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Search plugin
+class SearchPlugin(BasePlugin[SearchConfig]):
+
+ # Initialize plugin
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Initialize incremental builds
+ self.is_dirtyreload = False
+
+ # Initialize search index cache
+ self.search_index_prev = None
+
+ # Determine whether we're serving the site
+ def on_startup(self, *, command, dirty):
+ self.is_dirty = dirty
+
+ # Initialize plugin
+ def on_config(self, config):
+ if not self.config.enabled:
+ return
+
+ # Retrieve default value for language
+ if not self.config.lang:
+ self.config.lang = [self._translate(
+ config, "search.config.lang"
+ )]
+
+ # Retrieve default value for separator
+ if not self.config.separator:
+ self.config.separator = self._translate(
+ config, "search.config.separator"
+ )
+
+ # Retrieve default value for pipeline
+ if not self.config.pipeline:
+ self.config.pipeline = list(filter(len, re.split(
+ r"\s*,\s*", self._translate(config, "search.config.pipeline")
+ )))
+
+ # Initialize search index
+ self.search_index = SearchIndex(**self.config)
+
+ # Set jieba dictionary, if given
+ if self.config.jieba_dict:
+ path = os.path.normpath(self.config.jieba_dict)
+ if os.path.isfile(path):
+ jieba.set_dictionary(path)
+ log.debug(f"Loading jieba dictionary: {path}")
+ else:
+ log.warning(
+ f"Configuration error for 'search.jieba_dict': "
+ f"'{self.config.jieba_dict}' does not exist."
+ )
+
+ # Set jieba user dictionary, if given
+ if self.config.jieba_dict_user:
+ path = os.path.normpath(self.config.jieba_dict_user)
+ if os.path.isfile(path):
+ jieba.load_userdict(path)
+ log.debug(f"Loading jieba user dictionary: {path}")
+ else:
+ log.warning(
+ f"Configuration error for 'search.jieba_dict_user': "
+ f"'{self.config.jieba_dict_user}' does not exist."
+ )
+
+ # Add page to search index
+ def on_page_context(self, context, *, page, config, nav):
+ if not self.config.enabled:
+ return
+
+ # Index page
+ self.search_index.add_entry_from_context(page)
+ page.content = re.sub(
+ r"\s?data-search-\w+=\"[^\"]+\"",
+ "",
+ page.content
+ )
+
+ # Generate search index
+ def on_post_build(self, *, config):
+ if not self.config.enabled:
+ return
+
+ # Write search index
+ base = os.path.join(config.site_dir, "search")
+ path = os.path.join(base, "search_index.json")
+
+ # Generate and write search index to file
+ data = self.search_index.generate_search_index(self.search_index_prev)
+ utils.write_file(data.encode("utf-8"), path)
+
+ # Persist search index for repeated invocation
+ if self.is_dirty:
+ self.search_index_prev = self.search_index
+
+ # Determine whether we're running under dirty reload
+ def on_serve(self, server, *, config, builder):
+ self.is_dirtyreload = self.is_dirty
+
+ # -------------------------------------------------------------------------
+
+ # Translate the given placeholder value
+ def _translate(self, config, value):
+ env = config.theme.get_env()
+
+ # Load language template and return translation for placeholder
+ language = "partials/language.html"
+ template = env.get_template(language, None, { "config": config })
+ return template.module.t(value)
+
+# -----------------------------------------------------------------------------
+
+# Search index with support for additional fields
+class SearchIndex:
+
+ # Initialize search index
+ def __init__(self, **config):
+ self.config = config
+ self.entries = []
+
+ # Add page to search index
+ def add_entry_from_context(self, page):
+ search = page.meta.get("search", {})
+ if search.get("exclude"):
+ return
+
+ # Divide page content into sections
+ parser = Parser()
+ parser.feed(page.content)
+ parser.close()
+
+ # Add sections to index
+ for section in parser.data:
+ if not section.is_excluded():
+ self.create_entry_for_section(section, page.toc, page.url, page)
+
+ # Override: graceful indexing and additional fields
+ def create_entry_for_section(self, section, toc, url, page):
+ item = self._find_toc_by_id(toc, section.id)
+ if item:
+ url = url + item.url
+ elif section.id:
+ url = url + "#" + section.id
+
+ # Set page title as section title if none was given, which happens when
+ # the first headline in a Markdown document is not a h1 headline. Also,
+ # if a page title was set via front matter, use that even though a h1
+ # might be given or the page name was specified in nav in mkdocs.yml
+ if not section.title:
+ section.title = [str(page.meta.get("title", page.title))]
+
+ # Compute title and text
+ title = "".join(section.title).strip()
+ text = "".join(section.text).strip()
+
+ # Segment Chinese characters if jieba is available
+ if jieba:
+ title = self._segment_chinese(title)
+ text = self._segment_chinese(text)
+
+ # Create entry for section
+ entry = {
+ "location": url,
+ "title": title,
+ "text": text
+ }
+
+ # Set document tags
+ tags = page.meta.get("tags")
+ if isinstance(tags, list):
+ entry["tags"] = []
+ for name in tags:
+ if name and isinstance(name, (str, int, float, bool)):
+ entry["tags"].append(name)
+
+ # Set document boost
+ search = page.meta.get("search", {})
+ if "boost" in search:
+ entry["boost"] = search["boost"]
+
+ # Add entry to index
+ self.entries.append(entry)
+
+ # Generate search index
+ def generate_search_index(self, prev):
+ config = {
+ key: self.config[key]
+ for key in ["lang", "separator", "pipeline"]
+ }
+
+ # Hack: if we're running under dirty reload, the search index will only
+ # include the entries for the current page. However, MkDocs > 1.4 allows
+ # us to persist plugin state across rebuilds, which is exactly what we
+ # do by passing the previously built index to this method. Thus, we just
+ # remove the previous entries for the current page, and append the new
+ # entries to the end of the index, as order doesn't matter.
+ if prev and self.entries:
+ path = self.entries[0]["location"]
+
+ # Since we're sure that we're running under dirty reload, the list
+ # of entries will only contain sections for a single page. Thus, we
+ # use the first entry to remove all entries from the previous run
+ # that belong to the current page. The rationale behind this is that
+ # authors might add or remove section headers, so we need to make
+ # sure that sections are synchronized correctly.
+ entries = [
+ entry for entry in prev.entries
+ if not entry["location"].startswith(path)
+ ]
+
+ # Merge previous with current entries
+ self.entries = entries + self.entries
+
+ # Otherwise just set previous entries
+ if prev and not self.entries:
+ self.entries = prev.entries
+
+ # Return search index as JSON
+ data = { "config": config, "docs": self.entries }
+ return json.dumps(
+ data,
+ separators = (",", ":"),
+ default = str
+ )
+
+ # -------------------------------------------------------------------------
+
+ # Retrieve item for anchor
+ def _find_toc_by_id(self, toc, id):
+ for toc_item in toc:
+ if toc_item.id == id:
+ return toc_item
+
+ # Recurse into children of item
+ toc_item = self._find_toc_by_id(toc_item.children, id)
+ if toc_item is not None:
+ return toc_item
+
+ # No item found
+ return None
+
+ # Find and segment Chinese characters in string
+ def _segment_chinese(self, data):
+ expr = re.compile(r"(\p{IsHan}+)", re.UNICODE)
+
+ # Replace callback
+ def replace(match):
+ value = match.group(0)
+
+ # Replace occurrence in original string with segmented version and
+ # surround with zero-width whitespace for efficient indexing
+ return "".join([
+ "\u200b",
+ "\u200b".join(jieba.cut(value.encode("utf-8"))),
+ "\u200b",
+ ])
+
+ # Return string with segmented occurrences
+ return expr.sub(replace, data).strip("\u200b")
+
+# -----------------------------------------------------------------------------
+
+# HTML element
+class Element:
+ """
+ An element with attributes, essentially a small wrapper object for the
+ parser to access attributes in other callbacks than handle_starttag.
+ """
+
+ # Initialize HTML element
+ def __init__(self, tag, attrs = {}):
+ self.tag = tag
+ self.attrs = attrs
+
+ # String representation
+ def __repr__(self):
+ return self.tag
+
+ # Support comparison (compare by tag only)
+ def __eq__(self, other):
+ if other is Element:
+ return self.tag == other.tag
+ else:
+ return self.tag == other
+
+ # Support set operations
+ def __hash__(self):
+ return hash(self.tag)
+
+ # Check whether the element should be excluded
+ def is_excluded(self):
+ return "data-search-exclude" in self.attrs
+
+# -----------------------------------------------------------------------------
+
+# HTML section
+class Section:
+ """
+ A block of text with markup, preceded by a title (with markup), i.e., a
+ headline with a certain level (h1-h6). Internally used by the parser.
+ """
+
+ # Initialize HTML section
+ def __init__(self, el, depth = 0):
+ self.el = el
+ self.depth = depth
+
+ # Initialize section data
+ self.text = []
+ self.title = []
+ self.id = None
+
+ # String representation
+ def __repr__(self):
+ if self.id:
+ return "#".join([self.el.tag, self.id])
+ else:
+ return self.el.tag
+
+ # Check whether the section should be excluded
+ def is_excluded(self):
+ return self.el.is_excluded()
+
+# -----------------------------------------------------------------------------
+
+# HTML parser
+class Parser(HTMLParser):
+ """
+ This parser divides the given string of HTML into a list of sections, each
+ of which are preceded by a h1-h6 level heading. A white- and blacklist of
+ tags dictates which tags should be preserved as part of the index, and
+ which should be ignored in their entirety.
+ """
+
+ # Initialize HTML parser
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Tags to skip
+ self.skip = set([
+ "object", # Objects
+ "script", # Scripts
+ "style" # Styles
+ ])
+
+ # Tags to keep
+ self.keep = set([
+ "p", # Paragraphs
+ "code", "pre", # Code blocks
+ "li", "ol", "ul", # Lists
+ "sub", "sup" # Sub- and superscripts
+ ])
+
+ # Current context and section
+ self.context = []
+ self.section = None
+
+ # All parsed sections
+ self.data = []
+
+ # Called at the start of every HTML tag
+ def handle_starttag(self, tag, attrs):
+ attrs = dict(attrs)
+
+ # Ignore self-closing tags
+ el = Element(tag, attrs)
+ if not tag in void:
+ self.context.append(el)
+ else:
+ return
+
+ # Handle heading
+ if tag in ([f"h{x}" for x in range(1, 7)]):
+ depth = len(self.context)
+ if "id" in attrs:
+
+ # Ensure top-level section
+ if tag != "h1" and not self.data:
+ self.section = Section(Element("hx"), depth)
+ self.data.append(self.section)
+
+ # Set identifier, if not first section
+ self.section = Section(el, depth)
+ if self.data:
+ self.section.id = attrs["id"]
+
+ # Append section to list
+ self.data.append(self.section)
+
+ # Handle preface - ensure top-level section
+ if not self.section:
+ self.section = Section(Element("hx"))
+ self.data.append(self.section)
+
+ # Handle special cases to skip
+ for key, value in attrs.items():
+
+ # Skip block if explicitly excluded from search
+ if key == "data-search-exclude":
+ self.skip.add(el)
+ return
+
+ # Skip line numbers - see https://bit.ly/3GvubZx
+ if key == "class" and value == "linenodiv":
+ self.skip.add(el)
+ return
+
+ # Render opening tag if kept
+ if not self.skip.intersection(self.context):
+ if tag in self.keep:
+
+ # Check whether we're inside the section title
+ data = self.section.text
+ if self.section.el in self.context:
+ data = self.section.title
+
+ # Append to section title or text
+ data.append(f"<{tag}>")
+
+ # Called at the end of every HTML tag
+ def handle_endtag(self, tag):
+ if not self.context or self.context[-1] != tag:
+ return
+
+ # Check whether we're exiting the current context, which happens when
+ # a headline is nested in another element. In that case, we close the
+ # current section, continuing to append data to the previous section,
+ # which could also be a nested section – see https://bit.ly/3IxxIJZ
+ if self.section.depth > len(self.context):
+ for section in reversed(self.data):
+ if section.depth <= len(self.context):
+
+ # Set depth to infinity in order to denote that the current
+ # section is exited and must never be considered again.
+ self.section.depth = float("inf")
+ self.section = section
+ break
+
+ # Remove element from skip list
+ el = self.context.pop()
+ if el in self.skip:
+ if el.tag not in ["script", "style", "object"]:
+ self.skip.remove(el)
+ return
+
+ # Render closing tag if kept
+ if not self.skip.intersection(self.context):
+ if tag in self.keep:
+
+ # Check whether we're inside the section title
+ data = self.section.text
+ if self.section.el in self.context:
+ data = self.section.title
+
+ # Search for corresponding opening tag
+ index = data.index(f"<{tag}>")
+ for i in range(index + 1, len(data)):
+ if not data[i].isspace():
+ index = len(data)
+ break
+
+ # Remove element if empty (or only whitespace)
+ if len(data) > index:
+ while len(data) > index:
+ data.pop()
+
+ # Append to section title or text
+ else:
+ data.append(f"</{tag}>")
+
+ # Called for the text contents of each tag
+ def handle_data(self, data):
+ if self.skip.intersection(self.context):
+ return
+
+ # Collapse whitespace in non-pre contexts
+ if not "pre" in self.context:
+ if not data.isspace():
+ data = data.replace("\n", " ")
+ else:
+ data = " "
+
+ # Handle preface - ensure top-level section
+ if not self.section:
+ self.section = Section(Element("hx"))
+ self.data.append(self.section)
+
+ # Handle section headline
+ if self.section.el in self.context:
+ permalink = False
+ for el in self.context:
+ if el.tag == "a" and el.attrs.get("class") == "headerlink":
+ permalink = True
+
+ # Ignore permalinks
+ if not permalink:
+ self.section.title.append(
+ escape(data, quote = False)
+ )
+
+ # Collapse adjacent whitespace
+ elif data.isspace():
+ if not self.section.text or not self.section.text[-1].isspace():
+ self.section.text.append(data)
+ elif "pre" in self.context:
+ self.section.text.append(data)
+
+ # Handle everything else
+ else:
+ self.section.text.append(
+ escape(data, quote = False)
+ )
+
+# -----------------------------------------------------------------------------
+# Data
+# -----------------------------------------------------------------------------
+
+# Set up logging
+log = logging.getLogger("mkdocs.material.search")
+
+# Tags that are self-closing
+void = set([
+ "area", # Image map areas
+ "base", # Document base
+ "br", # Line breaks
+ "col", # Table columns
+ "embed", # External content
+ "hr", # Horizontal rules
+ "img", # Images
+ "input", # Input fields
+ "link", # Links
+ "meta", # Metadata
+ "param", # External parameters
+ "source", # Image source sets
+ "track", # Text track
+ "wbr" # Line break opportunities
+])
diff --git a/src/plugins/social/__init__.py b/src/plugins/social/__init__.py
new file mode 100644
index 00000000..d1899378
--- /dev/null
+++ b/src/plugins/social/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
diff --git a/src/plugins/social/config.py b/src/plugins/social/config.py
new file mode 100644
index 00000000..2d87c25e
--- /dev/null
+++ b/src/plugins/social/config.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from mkdocs.config.base import Config
+from mkdocs.config.config_options import Deprecated, Type
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Social plugin configuration
+class SocialConfig(Config):
+ enabled = Type(bool, default = True)
+ cache_dir = Type(str, default = ".cache/plugin/social")
+
+ # Settings for social cards
+ cards = Type(bool, default = True)
+ cards_dir = Type(str, default = "assets/images/social")
+ cards_layout_options = Type(dict, default = {})
+
+ # Deprecated settings
+ cards_color = Deprecated(
+ option_type = Type(dict, default = {}),
+ message =
+ "Deprecated, use 'cards_layout_options.background_color' "
+ "and 'cards_layout_options.color' with 'default' layout"
+ )
+ cards_font = Deprecated(
+ option_type = Type(str),
+ message = "Deprecated, use 'cards_layout_options.font_family'"
+ )
diff --git a/src/plugins/social/plugin.py b/src/plugins/social/plugin.py
new file mode 100644
index 00000000..3cdfa3ce
--- /dev/null
+++ b/src/plugins/social/plugin.py
@@ -0,0 +1,516 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# -----------------------------------------------------------------------------
+# Disclaimer
+# -----------------------------------------------------------------------------
+# Please note: this version of the social plugin is not actively development
+# anymore. Instead, Material for MkDocs Insiders ships a complete rewrite of
+# the plugin which is much more powerful and addresses all shortcomings of
+# this implementation. Additionally, the new social plugin allows to create
+# entirely custom social cards. You can probably imagine, that this was a lot
+# of work to pull off. If you run into problems, or want to have additional
+# functionality, please consider sponsoring the project. You can then use the
+# new version of the plugin immediately.
+# -----------------------------------------------------------------------------
+
+import concurrent.futures
+import functools
+import logging
+import os
+import posixpath
+import re
+import requests
+import sys
+
+from collections import defaultdict
+from hashlib import md5
+from io import BytesIO
+from mkdocs.commands.build import DuplicateFilter
+from mkdocs.exceptions import PluginError
+from mkdocs.plugins import BasePlugin
+from shutil import copyfile
+from tempfile import TemporaryFile
+from zipfile import ZipFile
+try:
+ from cairosvg import svg2png
+ from PIL import Image, ImageDraw, ImageFont
+except ImportError:
+ pass
+
+from .config import SocialConfig
+
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Social plugin
+class SocialPlugin(BasePlugin[SocialConfig]):
+
+ def __init__(self):
+ self._executor = concurrent.futures.ThreadPoolExecutor(4)
+
+ # Retrieve configuration
+ def on_config(self, config):
+ self.color = colors.get("indigo")
+ self.config.cards = self.config.enabled
+ if not self.config.cards:
+ return
+
+ # Check dependencies
+ if "Image" not in globals():
+ raise PluginError(
+ "Required dependencies of \"social\" plugin not found. "
+ "Install with: pip install \"mkdocs-material[imaging]\""
+ )
+
+ # Move color options
+ if self.config.cards_color:
+
+ # Move background color to new option
+ value = self.config.cards_color.get("fill")
+ if value:
+ self.config.cards_layout_options["background_color"] = value
+
+ # Move color to new option
+ value = self.config.cards_color.get("text")
+ if value:
+ self.config.cards_layout_options["color"] = value
+
+ # Move font family to new option
+ if self.config.cards_font:
+ value = self.config.cards_font
+ self.config.cards_layout_options["font_family"] = value
+
+ # Check if site URL is defined
+ if not config.site_url:
+ log.warning(
+ "The \"site_url\" option is not set. The cards are generated, "
+ "but not linked, so they won't be visible on social media."
+ )
+
+ # Ensure presence of cache directory
+ self.cache = self.config.cache_dir
+ if not os.path.isdir(self.cache):
+ os.makedirs(self.cache)
+
+ # Retrieve palette from theme configuration
+ theme = config.theme
+ if "palette" in theme:
+ palette = theme["palette"]
+
+ # Use first palette, if multiple are defined
+ if isinstance(palette, list):
+ palette = palette[0]
+
+ # Set colors according to palette
+ if "primary" in palette and palette["primary"]:
+ primary = palette["primary"].replace(" ", "-")
+ self.color = colors.get(primary, self.color)
+
+ # Retrieve color overrides
+ options = self.config.cards_layout_options
+ self.color = {
+ "fill": options.get("background_color", self.color["fill"]),
+ "text": options.get("color", self.color["text"])
+ }
+
+ # Retrieve logo and font
+ self._resized_logo_promise = self._executor.submit(self._load_resized_logo, config)
+ self.font = self._load_font(config)
+
+ self._image_promises = []
+
+ # Create social cards
+ def on_page_markdown(self, markdown, page, config, files):
+ if not self.config.cards:
+ return
+
+ # Resolve image directory
+ directory = self.config.cards_dir
+ file, _ = os.path.splitext(page.file.src_path)
+
+ # Resolve path of image
+ path = "{}.png".format(os.path.join(
+ config.site_dir,
+ directory,
+ file
+ ))
+
+ # Resolve path of image directory
+ directory = os.path.dirname(path)
+ if not os.path.isdir(directory):
+ os.makedirs(directory)
+
+ # Compute site name
+ site_name = config.site_name
+
+ # Compute page title and description
+ title = page.meta.get("title", page.title)
+ description = config.site_description or ""
+ if "description" in page.meta:
+ description = page.meta["description"]
+
+ # Check type of meta title - see https://t.ly/m1Us
+ if not isinstance(title, str):
+ log.error(
+ f"Page meta title of page '{page.file.src_uri}' must be a "
+ f"string, but is of type \"{type(title)}\"."
+ )
+ sys.exit(1)
+
+ # Check type of meta description - see https://t.ly/m1Us
+ if not isinstance(description, str):
+ log.error(
+ f"Page meta description of '{page.file.src_uri}' must be a "
+ f"string, but is of type \"{type(description)}\"."
+ )
+ sys.exit(1)
+
+ # Generate social card if not in cache
+ hash = md5("".join([
+ site_name,
+ str(title),
+ description
+ ]).encode("utf-8"))
+ file = os.path.join(self.cache, f"{hash.hexdigest()}.png")
+ self._image_promises.append(self._executor.submit(
+ self._cache_image,
+ cache_path = file, dest_path = path,
+ render_function = lambda: self._render_card(site_name, title, description)
+ ))
+
+ # Inject meta tags into page
+ meta = page.meta.get("meta", [])
+ page.meta["meta"] = meta + self._generate_meta(page, config)
+
+ def on_post_build(self, config):
+ if not self.config.cards:
+ return
+
+ # Check for exceptions
+ for promise in self._image_promises:
+ promise.result()
+
+ # -------------------------------------------------------------------------
+
+ # Render image to cache (if not present), then copy from cache to site
+ def _cache_image(self, cache_path, dest_path, render_function):
+ if not os.path.isfile(cache_path):
+ image = render_function()
+ image.save(cache_path)
+
+ # Copy file from cache
+ copyfile(cache_path, dest_path)
+
+ @functools.lru_cache(maxsize=None)
+ def _get_font(self, kind, size):
+ return ImageFont.truetype(self.font[kind], size)
+
+ # Render social card
+ def _render_card(self, site_name, title, description):
+ # Render background and logo
+ image = self._render_card_background((1200, 630), self.color["fill"])
+ image.alpha_composite(
+ self._resized_logo_promise.result(),
+ (1200 - 228, 64 - 4)
+ )
+
+ # Render site name
+ font = self._get_font("Bold", 36)
+ image.alpha_composite(
+ self._render_text((826, 48), font, site_name, 1, 20),
+ (64 + 4, 64)
+ )
+
+ # Render page title
+ font = self._get_font("Bold", 92)
+ image.alpha_composite(
+ self._render_text((826, 328), font, title, 3, 30),
+ (64, 160)
+ )
+
+ # Render page description
+ font = self._get_font("Regular", 28)
+ image.alpha_composite(
+ self._render_text((826, 80), font, description, 2, 14),
+ (64 + 4, 512)
+ )
+
+ # Return social card image
+ return image
+
+ # Render social card background
+ def _render_card_background(self, size, fill):
+ return Image.new(mode = "RGBA", size = size, color = fill)
+
+ @functools.lru_cache(maxsize=None)
+ def _tmp_context(self):
+ image = Image.new(mode = "RGBA", size = (50, 50))
+ return ImageDraw.Draw(image)
+
+ @functools.lru_cache(maxsize=None)
+ def _text_bounding_box(self, text, font):
+ return self._tmp_context().textbbox((0, 0), text, font = font)
+
+ # Render social card text
+ def _render_text(self, size, font, text, lmax, spacing = 0):
+ width = size[0]
+ lines, words = [], []
+
+ # Remove remnant HTML tags
+ text = re.sub(r"(<[^>]+>)", "", text)
+
+ # Retrieve y-offset of textbox to correct for spacing
+ yoffset = 0
+
+ # Create drawing context and split text into lines
+ for word in text.split(" "):
+ combine = " ".join(words + [word])
+ textbox = self._text_bounding_box(combine, font = font)
+ yoffset = textbox[1]
+ if not words or textbox[2] <= width:
+ words.append(word)
+ else:
+ lines.append(words)
+ words = [word]
+
+ # Join words for each line and create image
+ lines.append(words)
+ lines = [" ".join(line) for line in lines]
+ image = Image.new(mode = "RGBA", size = size)
+
+ # Create drawing context and split text into lines
+ context = ImageDraw.Draw(image)
+ context.text(
+ (0, spacing / 2 - yoffset), "\n".join(lines[:lmax]),
+ font = font, fill = self.color["text"], spacing = spacing - yoffset
+ )
+
+ # Return text image
+ return image
+
+ # -------------------------------------------------------------------------
+
+ # Generate meta tags
+ def _generate_meta(self, page, config):
+ directory = self.config.cards_dir
+ file, _ = os.path.splitext(page.file.src_uri)
+
+ # Compute page title
+ title = page.meta.get("title", page.title)
+ if not page.is_homepage:
+ title = f"{title} - {config.site_name}"
+
+ # Compute page description
+ description = config.site_description
+ if "description" in page.meta:
+ description = page.meta["description"]
+
+ # Resolve image URL
+ url = "{}.png".format(posixpath.join(
+ config.site_url or ".",
+ directory,
+ file
+ ))
+
+ # Ensure forward slashes
+ url = url.replace(os.path.sep, "/")
+
+ # Return meta tags
+ return [
+
+ # Meta tags for Open Graph
+ { "property": "og:type", "content": "website" },
+ { "property": "og:title", "content": title },
+ { "property": "og:description", "content": description },
+ { "property": "og:image", "content": url },
+ { "property": "og:image:type", "content": "image/png" },
+ { "property": "og:image:width", "content": "1200" },
+ { "property": "og:image:height", "content": "630" },
+ { "property": "og:url", "content": page.canonical_url },
+
+ # Meta tags for Twitter
+ { "name": "twitter:card", "content": "summary_large_image" },
+ # { "name": "twitter:site", "content": user },
+ # { "name": "twitter:creator", "content": user },
+ { "name": "twitter:title", "content": title },
+ { "name": "twitter:description", "content": description },
+ { "name": "twitter:image", "content": url }
+ ]
+
+ def _load_resized_logo(self, config, width = 144):
+ logo = self._load_logo(config)
+ height = int(width * logo.height / logo.width)
+ return logo.resize((width, height))
+
+ # Retrieve logo image or icon
+ def _load_logo(self, config):
+ theme = config.theme
+
+ # Handle images (precedence over icons)
+ if "logo" in theme:
+ _, extension = os.path.splitext(theme["logo"])
+
+ path = os.path.join(config.docs_dir, theme["logo"])
+
+ # Allow users to put the logo inside their custom_dir (theme["logo"] case)
+ if theme.custom_dir:
+ custom_dir_logo = os.path.join(theme.custom_dir, theme["logo"])
+ if os.path.exists(custom_dir_logo):
+ path = custom_dir_logo
+
+ # Load SVG and convert to PNG
+ if extension == ".svg":
+ return self._load_logo_svg(path)
+
+ # Load PNG, JPEG, etc.
+ return Image.open(path).convert("RGBA")
+
+ # Handle icons
+ icon = theme["icon"] or {}
+ if "logo" in icon and icon["logo"]:
+ logo = icon["logo"]
+ else:
+ logo = "material/library"
+
+ # Resolve path of package
+ base = os.path.abspath(os.path.join(
+ os.path.dirname(__file__),
+ "../.."
+ ))
+
+ path = f"{base}/templates/.icons/{logo}.svg"
+
+ # Allow users to put the logo inside their custom_dir (theme["icon"]["logo"] case)
+ if theme.custom_dir:
+ custom_dir_logo = os.path.join(theme.custom_dir, ".icons", f"{logo}.svg")
+ if os.path.exists(custom_dir_logo):
+ path = custom_dir_logo
+
+ # Load icon data and fill with color
+ return self._load_logo_svg(path, self.color["text"])
+
+ # Load SVG file and convert to PNG
+ def _load_logo_svg(self, path, fill = None):
+ file = BytesIO()
+ data = open(path).read()
+
+ # Fill with color, if given
+ if fill:
+ data = data.replace("<svg", f"<svg fill=\"{fill}\"")
+
+ # Convert to PNG and return image
+ svg2png(bytestring = data, write_to = file, scale = 10)
+ return Image.open(file)
+
+ # Retrieve font
+ def _load_font(self, config):
+ name = self.config.cards_layout_options.get("font_family")
+ if not name:
+
+ # Retrieve from theme (default: Roboto)
+ theme = config.theme
+ if isinstance(theme["font"], dict) and "text" in theme["font"]:
+ name = theme["font"]["text"]
+ else:
+ name = "Roboto"
+
+ # Google fonts can return varients like OpenSans_Condensed-Regular.ttf so
+ # we only use the font requested e.g. OpenSans-Regular.ttf
+ font_filename_base = name.replace(' ', '')
+ filename_regex = re.escape(font_filename_base)+r"-(\w+)\.[ot]tf$"
+
+ font = {}
+ # Check for cached files - note these may be in subfolders
+ for currentpath, folders, files in os.walk(self.cache):
+ for file in files:
+ # Map available font weights to file paths
+ fname = os.path.join(currentpath, file)
+ match = re.search(filename_regex, fname)
+ if match:
+ font[match.group(1)] = fname
+
+ # If none found, fetch from Google and try again
+ if len(font) == 0:
+ self._load_font_from_google(name)
+ for currentpath, folders, files in os.walk(self.cache):
+ for file in files:
+ # Map available font weights to file paths
+ fname = os.path.join(currentpath, file)
+ match = re.search(filename_regex, fname)
+ if match:
+ font[match.group(1)] = fname
+
+ # Return available font weights with fallback
+ return defaultdict(lambda: font["Regular"], font)
+
+ # Retrieve font from Google Fonts
+ def _load_font_from_google(self, name):
+ url = "https://fonts.google.com/download?family={}"
+ res = requests.get(url.format(name.replace(" ", "+")), stream = True)
+
+ # Write archive to temporary file
+ tmp = TemporaryFile()
+ for chunk in res.iter_content(chunk_size = 32768):
+ tmp.write(chunk)
+
+ # Unzip fonts from temporary file
+ zip = ZipFile(tmp)
+ files = [file for file in zip.namelist() if file.endswith(".ttf") or file.endswith(".otf")]
+ zip.extractall(self.cache, files)
+
+ # Close and delete temporary file
+ tmp.close()
+ return files
+
+# -----------------------------------------------------------------------------
+# Data
+# -----------------------------------------------------------------------------
+
+# Set up logging
+log = logging.getLogger("mkdocs")
+log.addFilter(DuplicateFilter())
+
+# Color palette
+colors = dict({
+ "red": { "fill": "#ef5552", "text": "#ffffff" },
+ "pink": { "fill": "#e92063", "text": "#ffffff" },
+ "purple": { "fill": "#ab47bd", "text": "#ffffff" },
+ "deep-purple": { "fill": "#7e56c2", "text": "#ffffff" },
+ "indigo": { "fill": "#4051b5", "text": "#ffffff" },
+ "blue": { "fill": "#2094f3", "text": "#ffffff" },
+ "light-blue": { "fill": "#02a6f2", "text": "#ffffff" },
+ "cyan": { "fill": "#00bdd6", "text": "#ffffff" },
+ "teal": { "fill": "#009485", "text": "#ffffff" },
+ "green": { "fill": "#4cae4f", "text": "#ffffff" },
+ "light-green": { "fill": "#8bc34b", "text": "#ffffff" },
+ "lime": { "fill": "#cbdc38", "text": "#000000" },
+ "yellow": { "fill": "#ffec3d", "text": "#000000" },
+ "amber": { "fill": "#ffc105", "text": "#000000" },
+ "orange": { "fill": "#ffa724", "text": "#000000" },
+ "deep-orange": { "fill": "#ff6e42", "text": "#ffffff" },
+ "brown": { "fill": "#795649", "text": "#ffffff" },
+ "grey": { "fill": "#757575", "text": "#ffffff" },
+ "blue-grey": { "fill": "#546d78", "text": "#ffffff" },
+ "black": { "fill": "#000000", "text": "#ffffff" },
+ "white": { "fill": "#ffffff", "text": "#000000" }
+})
diff --git a/src/plugins/tags/__init__.py b/src/plugins/tags/__init__.py
new file mode 100644
index 00000000..19994c95
--- /dev/null
+++ b/src/plugins/tags/__init__.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# -----------------------------------------------------------------------------
+# Functions
+# -----------------------------------------------------------------------------
+
+# Casefold a string for comparison when sorting
+def casefold(tag: str):
+ return tag.casefold()
diff --git a/src/plugins/tags/config.py b/src/plugins/tags/config.py
new file mode 100644
index 00000000..f2d95084
--- /dev/null
+++ b/src/plugins/tags/config.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from functools import partial
+from markdown.extensions.toc import slugify
+from mkdocs.config.config_options import Optional, Type
+from mkdocs.config.base import Config
+
+from . import casefold
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Tags plugin configuration
+class TagsConfig(Config):
+ enabled = Type(bool, default = True)
+
+ # Settings for tags
+ tags = Type(bool, default = True)
+ tags_file = Optional(Type(str))
diff --git a/src/plugins/tags/plugin.py b/src/plugins/tags/plugin.py
new file mode 100644
index 00000000..e5ce6bde
--- /dev/null
+++ b/src/plugins/tags/plugin.py
@@ -0,0 +1,182 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import logging
+import sys
+
+from collections import defaultdict
+from markdown.extensions.toc import slugify
+from mkdocs import utils
+from mkdocs.plugins import BasePlugin
+
+# deprecated, but kept for downward compatibility. Use 'material.plugins.tags'
+# as an import source instead. This import is removed in the next major version.
+from . import casefold
+from .config import TagsConfig
+
+# -----------------------------------------------------------------------------
+# Classes
+# -----------------------------------------------------------------------------
+
+# Tags plugin
+class TagsPlugin(BasePlugin[TagsConfig]):
+ supports_multiple_instances = True
+
+ # Initialize plugin
+ def on_config(self, config):
+ if not self.config.enabled:
+ return
+
+ # Skip if tags should not be built
+ if not self.config.tags:
+ return
+
+ # Initialize tags
+ self.tags = defaultdict(list)
+ self.tags_file = None
+
+ # Retrieve tags mapping from configuration
+ self.tags_map = config.extra.get("tags")
+
+ # Use override of slugify function
+ toc = { "slugify": slugify, "separator": "-" }
+ if "toc" in config.mdx_configs:
+ toc = { **toc, **config.mdx_configs["toc"] }
+
+ # Partially apply slugify function
+ self.slugify = lambda value: (
+ toc["slugify"](str(value), toc["separator"])
+ )
+
+ # Hack: 2nd pass for tags index page(s)
+ def on_nav(self, nav, config, files):
+ if not self.config.enabled:
+ return
+
+ # Skip if tags should not be built
+ if not self.config.tags:
+ return
+
+ # Resolve tags index page
+ file = self.config.tags_file
+ if file:
+ self.tags_file = self._get_tags_file(files, file)
+
+ # Build and render tags index page
+ def on_page_markdown(self, markdown, page, config, files):
+ if not self.config.enabled:
+ return
+
+ # Skip if tags should not be built
+ if not self.config.tags:
+ return
+
+ # Skip, if page is excluded
+ if page.file.inclusion.is_excluded():
+ return
+
+ # Render tags index page
+ if page.file == self.tags_file:
+ return self._render_tag_index(markdown)
+
+ # Add page to tags index
+ for tag in page.meta.get("tags", []):
+ self.tags[tag].append(page)
+
+ # Inject tags into page (after search and before minification)
+ def on_page_context(self, context, page, config, nav):
+ if not self.config.enabled:
+ return
+
+ # Skip if tags should not be built
+ if not self.config.tags:
+ return
+
+ # Provide tags for page
+ if "tags" in page.meta:
+ context["tags"] = [
+ self._render_tag(tag)
+ for tag in page.meta["tags"]
+ ]
+
+ # -------------------------------------------------------------------------
+
+ # Obtain tags file
+ def _get_tags_file(self, files, path):
+ file = files.get_file_from_path(path)
+ if not file:
+ log.error(f"Tags file '{path}' does not exist.")
+ sys.exit(1)
+
+ # Add tags file to files
+ files.append(file)
+ return file
+
+ # Render tags index
+ def _render_tag_index(self, markdown):
+ if not "[TAGS]" in markdown:
+ markdown += "\n[TAGS]"
+
+ # Replace placeholder in Markdown with rendered tags index
+ return markdown.replace("[TAGS]", "\n".join([
+ self._render_tag_links(*args)
+ for args in sorted(self.tags.items())
+ ]))
+
+ # Render the given tag and links to all pages with occurrences
+ def _render_tag_links(self, tag, pages):
+ classes = ["md-tag"]
+ if isinstance(self.tags_map, dict):
+ classes.append("md-tag-icon")
+ type = self.tags_map.get(tag)
+ if type:
+ classes.append(f"md-tag--{type}")
+
+ # Render section for tag and a link to each page
+ classes = " ".join(classes)
+ content = [f"## <span class=\"{classes}\">{tag}</span>", ""]
+ for page in pages:
+ url = utils.get_relative_url(
+ page.file.src_uri,
+ self.tags_file.src_uri
+ )
+
+ # Render link to page
+ title = page.meta.get("title", page.title)
+ content.append(f"- [{title}]({url})")
+
+ # Return rendered tag links
+ return "\n".join(content)
+
+ # Render the given tag, linking to the tags index (if enabled)
+ def _render_tag(self, tag):
+ type = self.tags_map.get(tag) if self.tags_map else None
+ if not self.tags_file or not self.slugify:
+ return dict(name = tag, type = type)
+ else:
+ url = f"{self.tags_file.url}#{self.slugify(tag)}"
+ return dict(name = tag, type = type, url = url)
+
+# -----------------------------------------------------------------------------
+# Data
+# -----------------------------------------------------------------------------
+
+# Set up logging
+log = logging.getLogger("mkdocs.material.tags")
diff --git a/src/templates/.icons/logo.afdesign b/src/templates/.icons/logo.afdesign
new file mode 100644
index 00000000..07f57d0a
--- /dev/null
+++ b/src/templates/.icons/logo.afdesign
Binary files differ
diff --git a/src/templates/.icons/logo.svg b/src/templates/.icons/logo.svg
new file mode 100644
index 00000000..763eb2c2
--- /dev/null
+++ b/src/templates/.icons/logo.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 89 89">
+ <path d="M3.136,17.387l0,42.932l42.932,21.467l-42.932,-64.399Z" />
+ <path d="M21.91,8l42.933,64.398l-18.775,9.388l-42.932,-64.399l18.774,-9.387Z" style="fill-opacity: 0.5" />
+ <path d="M67.535,17.387l-27.262,18.156l21.878,32.818l5.384,2.691l0,-53.665Z" />
+ <path d="M67.535,17.387l0,53.666l18.774,-9.388l0,-53.665l-18.774,9.387Z" style="fill-opacity: 0.25" />
+</svg>
diff --git a/src/templates/404.html b/src/templates/404.html
new file mode 100644
index 00000000..e87e7783
--- /dev/null
+++ b/src/templates/404.html
@@ -0,0 +1,28 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+{% extends "main.html" %}
+
+<!-- Content -->
+{% block content %}
+ <h1>404 - Not found</h1>
+{% endblock %}
diff --git a/src/templates/__init__.py b/src/templates/__init__.py
new file mode 100644
index 00000000..d1899378
--- /dev/null
+++ b/src/templates/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
diff --git a/src/templates/assets/images/favicon.png b/src/templates/assets/images/favicon.png
new file mode 100644
index 00000000..1cf13b9f
--- /dev/null
+++ b/src/templates/assets/images/favicon.png
Binary files differ
diff --git a/src/templates/assets/javascripts/_/index.ts b/src/templates/assets/javascripts/_/index.ts
new file mode 100644
index 00000000..be0f4a42
--- /dev/null
+++ b/src/templates/assets/javascripts/_/index.ts
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { getElement, getLocation } from "~/browser"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Feature flag
+ */
+export type Flag =
+ | "announce.dismiss" /* Dismissable announcement bar */
+ | "content.code.annotate" /* Code annotations */
+ | "content.code.copy" /* Code copy button */
+ | "content.lazy" /* Lazy content elements */
+ | "content.tabs.link" /* Link content tabs */
+ | "header.autohide" /* Hide header */
+ | "navigation.expand" /* Automatic expansion */
+ | "navigation.indexes" /* Section pages */
+ | "navigation.instant" /* Instant navigation */
+ | "navigation.instant.progress" /* Instant navigation progress */
+ | "navigation.sections" /* Section navigation */
+ | "navigation.tabs" /* Tabs navigation */
+ | "navigation.tabs.sticky" /* Tabs navigation (sticky) */
+ | "navigation.top" /* Back-to-top button */
+ | "navigation.tracking" /* Anchor tracking */
+ | "search.highlight" /* Search highlighting */
+ | "search.share" /* Search sharing */
+ | "search.suggest" /* Search suggestions */
+ | "toc.follow" /* Following table of contents */
+ | "toc.integrate" /* Integrated table of contents */
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Translation
+ */
+export type Translation =
+ | "clipboard.copy" /* Copy to clipboard */
+ | "clipboard.copied" /* Copied to clipboard */
+ | "search.result.placeholder" /* Type to start searching */
+ | "search.result.none" /* No matching documents */
+ | "search.result.one" /* 1 matching document */
+ | "search.result.other" /* # matching documents */
+ | "search.result.more.one" /* 1 more on this page */
+ | "search.result.more.other" /* # more on this page */
+ | "search.result.term.missing" /* Missing */
+ | "select.version" /* Version selector */
+
+/**
+ * Translations
+ */
+export type Translations =
+ Record<Translation, string>
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Versioning
+ */
+export interface Versioning {
+ provider: "mike" /* Version provider */
+ default?: string | string[] /* Default version */
+}
+
+/**
+ * Configuration
+ */
+export interface Config {
+ base: string /* Base URL */
+ features: Flag[] /* Feature flags */
+ translations: Translations /* Translations */
+ search: string /* Search worker URL */
+ tags?: Record<string, string> /* Tags mapping */
+ version?: Versioning /* Versioning */
+}
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve global configuration and make base URL absolute
+ */
+const script = getElement("#__config")
+const config: Config = JSON.parse(script.textContent!)
+config.base = `${new URL(config.base, getLocation())}`
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve global configuration
+ *
+ * @returns Global configuration
+ */
+export function configuration(): Config {
+ return config
+}
+
+/**
+ * Check whether a feature flag is enabled
+ *
+ * @param flag - Feature flag
+ *
+ * @returns Test result
+ */
+export function feature(flag: Flag): boolean {
+ return config.features.includes(flag)
+}
+
+/**
+ * Retrieve the translation for the given key
+ *
+ * @param key - Key to be translated
+ * @param value - Positional value, if any
+ *
+ * @returns Translation
+ */
+export function translation(
+ key: Translation, value?: string | number
+): string {
+ return typeof value !== "undefined"
+ ? config.translations[key].replace("#", value.toString())
+ : config.translations[key]
+}
diff --git a/src/templates/assets/javascripts/browser/document/index.ts b/src/templates/assets/javascripts/browser/document/index.ts
new file mode 100644
index 00000000..354c9b5c
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/document/index.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ ReplaySubject,
+ Subject,
+ fromEvent
+} from "rxjs"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch document
+ *
+ * Documents are implemented as subjects, so all downstream observables are
+ * automatically updated when a new document is emitted.
+ *
+ * @returns Document subject
+ */
+export function watchDocument(): Subject<Document> {
+ const document$ = new ReplaySubject<Document>(1)
+ fromEvent(document, "DOMContentLoaded", { once: true })
+ .subscribe(() => document$.next(document))
+
+ /* Return document */
+ return document$
+}
diff --git a/src/templates/assets/javascripts/browser/element/_/.eslintrc b/src/templates/assets/javascripts/browser/element/_/.eslintrc
new file mode 100644
index 00000000..16973760
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/_/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "rules": {
+ "jsdoc/require-jsdoc": "off",
+ "jsdoc/require-returns-check": "off"
+ }
+}
diff --git a/src/templates/assets/javascripts/browser/element/_/index.ts b/src/templates/assets/javascripts/browser/element/_/index.ts
new file mode 100644
index 00000000..b7beb462
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/_/index.ts
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve all elements matching the query selector
+ *
+ * @template T - Element type
+ *
+ * @param selector - Query selector
+ * @param node - Node of reference
+ *
+ * @returns Elements
+ */
+export function getElements<T extends keyof HTMLElementTagNameMap>(
+ selector: T, node?: ParentNode
+): HTMLElementTagNameMap[T][]
+
+export function getElements<T extends HTMLElement>(
+ selector: string, node?: ParentNode
+): T[]
+
+export function getElements<T extends HTMLElement>(
+ selector: string, node: ParentNode = document
+): T[] {
+ return Array.from(node.querySelectorAll<T>(selector))
+}
+
+/**
+ * Retrieve an element matching a query selector or throw a reference error
+ *
+ * Note that this function assumes that the element is present. If unsure if an
+ * element is existent, use the `getOptionalElement` function instead.
+ *
+ * @template T - Element type
+ *
+ * @param selector - Query selector
+ * @param node - Node of reference
+ *
+ * @returns Element
+ */
+export function getElement<T extends keyof HTMLElementTagNameMap>(
+ selector: T, node?: ParentNode
+): HTMLElementTagNameMap[T]
+
+export function getElement<T extends HTMLElement>(
+ selector: string, node?: ParentNode
+): T
+
+export function getElement<T extends HTMLElement>(
+ selector: string, node: ParentNode = document
+): T {
+ const el = getOptionalElement<T>(selector, node)
+ if (typeof el === "undefined")
+ throw new ReferenceError(
+ `Missing element: expected "${selector}" to be present`
+ )
+
+ /* Return element */
+ return el
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve an optional element matching the query selector
+ *
+ * @template T - Element type
+ *
+ * @param selector - Query selector
+ * @param node - Node of reference
+ *
+ * @returns Element or nothing
+ */
+export function getOptionalElement<T extends keyof HTMLElementTagNameMap>(
+ selector: T, node?: ParentNode
+): HTMLElementTagNameMap[T] | undefined
+
+export function getOptionalElement<T extends HTMLElement>(
+ selector: string, node?: ParentNode
+): T | undefined
+
+export function getOptionalElement<T extends HTMLElement>(
+ selector: string, node: ParentNode = document
+): T | undefined {
+ return node.querySelector<T>(selector) || undefined
+}
+
+/**
+ * Retrieve the currently active element
+ *
+ * @returns Element or nothing
+ */
+export function getActiveElement(): HTMLElement | undefined {
+ return document.activeElement instanceof HTMLElement
+ ? document.activeElement || undefined
+ : undefined
+}
diff --git a/src/templates/assets/javascripts/browser/element/focus/index.ts b/src/templates/assets/javascripts/browser/element/focus/index.ts
new file mode 100644
index 00000000..f31fe276
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/focus/index.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ debounceTime,
+ distinctUntilChanged,
+ fromEvent,
+ map,
+ merge,
+ shareReplay,
+ startWith
+} from "rxjs"
+
+import { getActiveElement } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Focus observable
+ *
+ * Previously, this observer used `focus` and `blur` events to determine whether
+ * an element is focused, but this doesn't work if there are focusable elements
+ * within the elements itself. A better solutions are `focusin` and `focusout`
+ * events, which bubble up the tree and allow for more fine-grained control.
+ *
+ * `debounceTime` is necessary, because when a focus change happens inside an
+ * element, the observable would first emit `false` and then `true` again.
+ */
+const observer$ = merge(
+ fromEvent(document.body, "focusin"),
+ fromEvent(document.body, "focusout")
+)
+ .pipe(
+ debounceTime(1),
+ startWith(undefined),
+ map(() => getActiveElement() || document.body),
+ shareReplay(1)
+ )
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch element focus
+ *
+ * @param el - Element
+ *
+ * @returns Element focus observable
+ */
+export function watchElementFocus(
+ el: HTMLElement
+): Observable<boolean> {
+ return observer$
+ .pipe(
+ map(active => el.contains(active)),
+ distinctUntilChanged()
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/element/index.ts b/src/templates/assets/javascripts/browser/element/index.ts
new file mode 100644
index 00000000..50ce84b2
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./focus"
+export * from "./offset"
+export * from "./size"
+export * from "./visibility"
diff --git a/src/templates/assets/javascripts/browser/element/offset/_/index.ts b/src/templates/assets/javascripts/browser/element/offset/_/index.ts
new file mode 100644
index 00000000..6dd229d5
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/offset/_/index.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ animationFrameScheduler,
+ auditTime,
+ fromEvent,
+ map,
+ merge,
+ startWith
+} from "rxjs"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Element offset
+ */
+export interface ElementOffset {
+ x: number /* Horizontal offset */
+ y: number /* Vertical offset */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve element offset
+ *
+ * @param el - Element
+ *
+ * @returns Element offset
+ */
+export function getElementOffset(
+ el: HTMLElement
+): ElementOffset {
+ return {
+ x: el.offsetLeft,
+ y: el.offsetTop
+ }
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Watch element offset
+ *
+ * @param el - Element
+ *
+ * @returns Element offset observable
+ */
+export function watchElementOffset(
+ el: HTMLElement
+): Observable<ElementOffset> {
+ return merge(
+ fromEvent(window, "load"),
+ fromEvent(window, "resize")
+ )
+ .pipe(
+ auditTime(0, animationFrameScheduler),
+ map(() => getElementOffset(el)),
+ startWith(getElementOffset(el))
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/element/offset/content/index.ts b/src/templates/assets/javascripts/browser/element/offset/content/index.ts
new file mode 100644
index 00000000..557301a6
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/offset/content/index.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ animationFrameScheduler,
+ auditTime,
+ fromEvent,
+ map,
+ merge,
+ startWith
+} from "rxjs"
+
+import { ElementOffset } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve element content offset (= scroll offset)
+ *
+ * @param el - Element
+ *
+ * @returns Element content offset
+ */
+export function getElementContentOffset(
+ el: HTMLElement
+): ElementOffset {
+ return {
+ x: el.scrollLeft,
+ y: el.scrollTop
+ }
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Watch element content offset
+ *
+ * @param el - Element
+ *
+ * @returns Element content offset observable
+ */
+export function watchElementContentOffset(
+ el: HTMLElement
+): Observable<ElementOffset> {
+ return merge(
+ fromEvent(el, "scroll"),
+ fromEvent(window, "resize")
+ )
+ .pipe(
+ auditTime(0, animationFrameScheduler),
+ map(() => getElementContentOffset(el)),
+ startWith(getElementContentOffset(el))
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/element/offset/index.ts b/src/templates/assets/javascripts/browser/element/offset/index.ts
new file mode 100644
index 00000000..602ff2cf
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/offset/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./content"
diff --git a/src/templates/assets/javascripts/browser/element/size/_/index.ts b/src/templates/assets/javascripts/browser/element/size/_/index.ts
new file mode 100644
index 00000000..35a5e68b
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/size/_/index.ts
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ NEVER,
+ Observable,
+ Subject,
+ defer,
+ filter,
+ finalize,
+ map,
+ merge,
+ of,
+ shareReplay,
+ startWith,
+ switchMap,
+ tap
+} from "rxjs"
+
+import { watchScript } from "../../../script"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Element offset
+ */
+export interface ElementSize {
+ width: number /* Element width */
+ height: number /* Element height */
+}
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Resize observer entry subject
+ */
+const entry$ = new Subject<ResizeObserverEntry>()
+
+/**
+ * Resize observer observable
+ *
+ * This observable will create a `ResizeObserver` on the first subscription
+ * and will automatically terminate it when there are no more subscribers.
+ * It's quite important to centralize observation in a single `ResizeObserver`,
+ * as the performance difference can be quite dramatic, as the link shows.
+ *
+ * If the browser doesn't have a `ResizeObserver` implementation available, a
+ * polyfill is automatically downloaded from unpkg.com. This is also compatible
+ * with the built-in privacy plugin, which will download the polyfill and put
+ * it alongside the built site for self-hosting.
+ *
+ * @see https://bit.ly/3iIYfEm - Google Groups on performance
+ */
+const observer$ = defer(() => (
+ typeof ResizeObserver === "undefined"
+ ? watchScript("https://unpkg.com/resize-observer-polyfill")
+ : of(undefined)
+))
+ .pipe(
+ map(() => new ResizeObserver(entries => {
+ for (const entry of entries)
+ entry$.next(entry)
+ })),
+ switchMap(observer => merge(NEVER, of(observer))
+ .pipe(
+ finalize(() => observer.disconnect())
+ )
+ ),
+ shareReplay(1)
+ )
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve element size
+ *
+ * @param el - Element
+ *
+ * @returns Element size
+ */
+export function getElementSize(
+ el: HTMLElement
+): ElementSize {
+ return {
+ width: el.offsetWidth,
+ height: el.offsetHeight
+ }
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Watch element size
+ *
+ * This function returns an observable that subscribes to a single internal
+ * instance of `ResizeObserver` upon subscription, and emit resize events until
+ * termination. Note that this function should not be called with the same
+ * element twice, as the first unsubscription will terminate observation.
+ *
+ * Sadly, we can't use the `DOMRect` objects returned by the observer, because
+ * we need the emitted values to be consistent with `getElementSize`, which will
+ * return the used values (rounded) and not actual values (unrounded). Thus, we
+ * use the `offset*` properties. See the linked GitHub issue.
+ *
+ * @see https://bit.ly/3m0k3he - GitHub issue
+ *
+ * @param el - Element
+ *
+ * @returns Element size observable
+ */
+export function watchElementSize(
+ el: HTMLElement
+): Observable<ElementSize> {
+ return observer$
+ .pipe(
+ tap(observer => observer.observe(el)),
+ switchMap(observer => entry$
+ .pipe(
+ filter(({ target }) => target === el),
+ finalize(() => observer.unobserve(el)),
+ map(() => getElementSize(el))
+ )
+ ),
+ startWith(getElementSize(el))
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/element/size/content/index.ts b/src/templates/assets/javascripts/browser/element/size/content/index.ts
new file mode 100644
index 00000000..5ed388cf
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/size/content/index.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { ElementSize } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve element content size (= scroll width and height)
+ *
+ * @param el - Element
+ *
+ * @returns Element content size
+ */
+export function getElementContentSize(
+ el: HTMLElement
+): ElementSize {
+ return {
+ width: el.scrollWidth,
+ height: el.scrollHeight
+ }
+}
+
+/**
+ * Retrieve the overflowing container of an element, if any
+ *
+ * @param el - Element
+ *
+ * @returns Overflowing container or nothing
+ */
+export function getElementContainer(
+ el: HTMLElement
+): HTMLElement | undefined {
+ let parent = el.parentElement
+ while (parent)
+ if (
+ el.scrollWidth <= parent.scrollWidth &&
+ el.scrollHeight <= parent.scrollHeight
+ )
+ parent = (el = parent).parentElement
+ else
+ break
+
+ /* Return overflowing container */
+ return parent ? el : undefined
+}
diff --git a/src/templates/assets/javascripts/browser/element/size/index.ts b/src/templates/assets/javascripts/browser/element/size/index.ts
new file mode 100644
index 00000000..602ff2cf
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/size/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./content"
diff --git a/src/templates/assets/javascripts/browser/element/visibility/index.ts b/src/templates/assets/javascripts/browser/element/visibility/index.ts
new file mode 100644
index 00000000..1ffe0b8d
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/element/visibility/index.ts
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ NEVER,
+ Observable,
+ Subject,
+ defer,
+ distinctUntilChanged,
+ filter,
+ finalize,
+ map,
+ merge,
+ of,
+ shareReplay,
+ switchMap,
+ tap
+} from "rxjs"
+
+import {
+ getElementContentSize,
+ getElementSize,
+ watchElementContentOffset
+} from "~/browser"
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Intersection observer entry subject
+ */
+const entry$ = new Subject<IntersectionObserverEntry>()
+
+/**
+ * Intersection observer observable
+ *
+ * This observable will create an `IntersectionObserver` on first subscription
+ * and will automatically terminate it when there are no more subscribers.
+ *
+ * @see https://bit.ly/3iIYfEm - Google Groups on performance
+ */
+const observer$ = defer(() => of(
+ new IntersectionObserver(entries => {
+ for (const entry of entries)
+ entry$.next(entry)
+ }, {
+ threshold: 0
+ })
+))
+ .pipe(
+ switchMap(observer => merge(NEVER, of(observer))
+ .pipe(
+ finalize(() => observer.disconnect())
+ )
+ ),
+ shareReplay(1)
+ )
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch element visibility
+ *
+ * @param el - Element
+ *
+ * @returns Element visibility observable
+ */
+export function watchElementVisibility(
+ el: HTMLElement
+): Observable<boolean> {
+ return observer$
+ .pipe(
+ tap(observer => observer.observe(el)),
+ switchMap(observer => entry$
+ .pipe(
+ filter(({ target }) => target === el),
+ finalize(() => observer.unobserve(el)),
+ map(({ isIntersecting }) => isIntersecting)
+ )
+ )
+ )
+}
+
+/**
+ * Watch element boundary
+ *
+ * This function returns an observable which emits whether the bottom content
+ * boundary (= scroll offset) of an element is within a certain threshold.
+ *
+ * @param el - Element
+ * @param threshold - Threshold
+ *
+ * @returns Element boundary observable
+ */
+export function watchElementBoundary(
+ el: HTMLElement, threshold = 16
+): Observable<boolean> {
+ return watchElementContentOffset(el)
+ .pipe(
+ map(({ y }) => {
+ const visible = getElementSize(el)
+ const content = getElementContentSize(el)
+ return y >= (
+ content.height - visible.height - threshold
+ )
+ }),
+ distinctUntilChanged()
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/index.ts b/src/templates/assets/javascripts/browser/index.ts
new file mode 100644
index 00000000..f1ee2bae
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/index.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./document"
+export * from "./element"
+export * from "./keyboard"
+export * from "./location"
+export * from "./media"
+export * from "./request"
+export * from "./script"
+export * from "./toggle"
+export * from "./viewport"
+export * from "./worker"
diff --git a/src/templates/assets/javascripts/browser/keyboard/index.ts b/src/templates/assets/javascripts/browser/keyboard/index.ts
new file mode 100644
index 00000000..783f2cda
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/keyboard/index.ts
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Observable,
+ filter,
+ fromEvent,
+ map,
+ merge,
+ share,
+ startWith,
+ switchMap
+} from "rxjs"
+
+import { getActiveElement } from "../element"
+import { getToggle } from "../toggle"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Keyboard mode
+ */
+export type KeyboardMode =
+ | "global" /* Global */
+ | "search" /* Search is open */
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Keyboard
+ */
+export interface Keyboard {
+ mode: KeyboardMode /* Keyboard mode */
+ type: string /* Key type */
+ claim(): void /* Key claim */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Check whether an element may receive keyboard input
+ *
+ * @param el - Element
+ * @param type - Key type
+ *
+ * @returns Test result
+ */
+function isSusceptibleToKeyboard(
+ el: HTMLElement, type: string
+): boolean {
+ switch (el.constructor) {
+
+ /* Input elements */
+ case HTMLInputElement:
+ /* @ts-expect-error - omit unnecessary type cast */
+ if (el.type === "radio")
+ return /^Arrow/.test(type)
+ else
+ return true
+
+ /* Select element and textarea */
+ case HTMLSelectElement:
+ case HTMLTextAreaElement:
+ return true
+
+ /* Everything else */
+ default:
+ return el.isContentEditable
+ }
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch composition events
+ *
+ * @returns Composition observable
+ */
+export function watchComposition(): Observable<boolean> {
+ return merge(
+ fromEvent(window, "compositionstart").pipe(map(() => true)),
+ fromEvent(window, "compositionend").pipe(map(() => false))
+ )
+ .pipe(
+ startWith(false)
+ )
+}
+
+/**
+ * Watch keyboard
+ *
+ * @returns Keyboard observable
+ */
+export function watchKeyboard(): Observable<Keyboard> {
+ const keyboard$ = fromEvent<KeyboardEvent>(window, "keydown")
+ .pipe(
+ filter(ev => !(ev.metaKey || ev.ctrlKey)),
+ map(ev => ({
+ mode: getToggle("search") ? "search" : "global",
+ type: ev.key,
+ claim() {
+ ev.preventDefault()
+ ev.stopPropagation()
+ }
+ } as Keyboard)),
+ filter(({ mode, type }) => {
+ if (mode === "global") {
+ const active = getActiveElement()
+ if (typeof active !== "undefined")
+ return !isSusceptibleToKeyboard(active, type)
+ }
+ return true
+ }),
+ share()
+ )
+
+ /* Don't emit during composition events - see https://bit.ly/3te3Wl8 */
+ return watchComposition()
+ .pipe(
+ switchMap(active => !active ? keyboard$ : EMPTY)
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/location/_/index.ts b/src/templates/assets/javascripts/browser/location/_/index.ts
new file mode 100644
index 00000000..2672fa74
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/location/_/index.ts
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { Subject } from "rxjs"
+
+import { feature } from "~/_"
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve location
+ *
+ * This function returns a `URL` object (and not `Location`) to normalize the
+ * typings across the application. Furthermore, locations need to be tracked
+ * without setting them and `Location` is a singleton which represents the
+ * current location.
+ *
+ * @returns URL
+ */
+export function getLocation(): URL {
+ return new URL(location.href)
+}
+
+/**
+ * Set location
+ *
+ * If instant navigation is enabled, this function creates a temporary anchor
+ * element, sets the `href` attribute, appends it to the body, clicks it, and
+ * then removes it again. The event will bubble up the DOM and trigger be
+ * intercepted by the instant loading business logic.
+ *
+ * Note that we must append and remove the anchor element, or the event will
+ * not bubble up the DOM, making it impossible to intercept it.
+ *
+ * @param url - URL to navigate to
+ * @param navigate - Force navigation
+ */
+export function setLocation(
+ url: URL | HTMLLinkElement, navigate = false
+): void {
+ if (feature("navigation.instant") && !navigate) {
+ const el = h("a", { href: url.href })
+ document.body.appendChild(el)
+ el.click()
+ el.remove()
+
+ // If we're not using instant navigation, and the page should not be reloaded
+ // just instruct the browser to navigate to the given URL
+ } else {
+ location.href = url.href
+ }
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Watch location
+ *
+ * @returns Location subject
+ */
+export function watchLocation(): Subject<URL> {
+ return new Subject<URL>()
+}
diff --git a/src/templates/assets/javascripts/browser/location/hash/index.ts b/src/templates/assets/javascripts/browser/location/hash/index.ts
new file mode 100644
index 00000000..5d3a134a
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/location/hash/index.ts
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ filter,
+ fromEvent,
+ map,
+ merge,
+ shareReplay,
+ startWith
+} from "rxjs"
+
+import { getOptionalElement } from "~/browser"
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve location hash
+ *
+ * @returns Location hash
+ */
+export function getLocationHash(): string {
+ return location.hash.slice(1)
+}
+
+/**
+ * Set location hash
+ *
+ * Setting a new fragment identifier via `location.hash` will have no effect
+ * if the value doesn't change. When a new fragment identifier is set, we want
+ * the browser to target the respective element at all times, which is why we
+ * use this dirty little trick.
+ *
+ * @param hash - Location hash
+ */
+export function setLocationHash(hash: string): void {
+ const el = h("a", { href: hash })
+ el.addEventListener("click", ev => ev.stopPropagation())
+ el.click()
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Watch location hash
+ *
+ * @param location$ - Location observable
+ *
+ * @returns Location hash observable
+ */
+export function watchLocationHash(
+ location$: Observable<URL>
+): Observable<string> {
+ return merge(
+ fromEvent<HashChangeEvent>(window, "hashchange"),
+ location$
+ )
+ .pipe(
+ map(getLocationHash),
+ startWith(getLocationHash()),
+ filter(hash => hash.length > 0),
+ shareReplay(1)
+ )
+}
+
+/**
+ * Watch location target
+ *
+ * @param location$ - Location observable
+ *
+ * @returns Location target observable
+ */
+export function watchLocationTarget(
+ location$: Observable<URL>
+): Observable<HTMLElement> {
+ return watchLocationHash(location$)
+ .pipe(
+ map(id => getOptionalElement(`[id="${id}"]`)!),
+ filter(el => typeof el !== "undefined")
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/location/index.ts b/src/templates/assets/javascripts/browser/location/index.ts
new file mode 100644
index 00000000..d77a5444
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/location/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./hash"
diff --git a/src/templates/assets/javascripts/browser/media/index.ts b/src/templates/assets/javascripts/browser/media/index.ts
new file mode 100644
index 00000000..dd7400d4
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/media/index.ts
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Observable,
+ fromEvent,
+ fromEventPattern,
+ map,
+ merge,
+ startWith,
+ switchMap
+} from "rxjs"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch media query
+ *
+ * Note that although `MediaQueryList.addListener` is deprecated we have to
+ * use it, because it's the only way to ensure proper downward compatibility.
+ *
+ * @see https://bit.ly/3dUBH2m - GitHub issue
+ *
+ * @param query - Media query
+ *
+ * @returns Media observable
+ */
+export function watchMedia(query: string): Observable<boolean> {
+ const media = matchMedia(query)
+ return fromEventPattern<boolean>(next => (
+ media.addListener(() => next(media.matches))
+ ))
+ .pipe(
+ startWith(media.matches)
+ )
+}
+
+/**
+ * Watch print mode
+ *
+ * @returns Print observable
+ */
+export function watchPrint(): Observable<boolean> {
+ const media = matchMedia("print")
+ return merge(
+ fromEvent(window, "beforeprint").pipe(map(() => true)),
+ fromEvent(window, "afterprint").pipe(map(() => false))
+ )
+ .pipe(
+ startWith(media.matches)
+ )
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Toggle an observable with a media observable
+ *
+ * @template T - Data type
+ *
+ * @param query$ - Media observable
+ * @param factory - Observable factory
+ *
+ * @returns Toggled observable
+ */
+export function at<T>(
+ query$: Observable<boolean>, factory: () => Observable<T>
+): Observable<T> {
+ return query$
+ .pipe(
+ switchMap(active => active ? factory() : EMPTY)
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/request/index.ts b/src/templates/assets/javascripts/browser/request/index.ts
new file mode 100644
index 00000000..74a56a64
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/request/index.ts
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ map,
+ shareReplay,
+ switchMap
+} from "rxjs"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Options
+ */
+interface Options {
+ progress$?: Subject<number> // Progress subject
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Fetch the given URL
+ *
+ * If the request fails (e.g. when dispatched from `file://` locations), the
+ * observable will complete without emitting a value.
+ *
+ * @param url - Request URL
+ * @param options - Options
+ *
+ * @returns Response observable
+ */
+export function request(
+ url: URL | string, options?: Options
+): Observable<Blob> {
+ return new Observable<Blob>(observer => {
+ const req = new XMLHttpRequest()
+ req.open("GET", `${url}`)
+ req.responseType = "blob"
+
+ // Handle response
+ req.addEventListener("load", () => {
+ if (req.status >= 200 && req.status < 300) {
+ observer.next(req.response)
+ observer.complete()
+ } else {
+ observer.error(new Error(req.statusText))
+ }
+ })
+
+ // Handle network errors
+ req.addEventListener("error", () => {
+ observer.error(new Error("Network Error"))
+ })
+
+ // Handle aborted requests
+ req.addEventListener("abort", () => {
+ observer.error(new Error("Request aborted"))
+ })
+
+ // Handle download progress
+ if (typeof options?.progress$ !== "undefined") {
+ req.addEventListener("progress", event => {
+ options.progress$!.next((event.loaded / event.total) * 100)
+ })
+
+ // Immediately set progress to 5% to indicate that we're loading
+ options.progress$.next(5)
+ }
+
+ // Send request
+ req.send()
+ })
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Fetch JSON from the given URL
+ *
+ * @template T - Data type
+ *
+ * @param url - Request URL
+ * @param options - Options
+ *
+ * @returns Data observable
+ */
+export function requestJSON<T>(
+ url: URL | string, options?: Options
+): Observable<T> {
+ return request(url, options)
+ .pipe(
+ switchMap(res => res.text()),
+ map(body => JSON.parse(body) as T),
+ shareReplay(1)
+ )
+}
+
+/**
+ * Fetch XML from the given URL
+ *
+ * @param url - Request URL
+ * @param options - Options
+ *
+ * @returns Data observable
+ */
+export function requestXML(
+ url: URL | string, options?: Options
+): Observable<Document> {
+ const dom = new DOMParser()
+ return request(url, options)
+ .pipe(
+ switchMap(res => res.text()),
+ map(res => dom.parseFromString(res, "text/xml")),
+ shareReplay(1)
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/script/index.ts b/src/templates/assets/javascripts/browser/script/index.ts
new file mode 100644
index 00000000..ef5c89e6
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/script/index.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ defer,
+ finalize,
+ fromEvent,
+ map,
+ merge,
+ switchMap,
+ take,
+ throwError
+} from "rxjs"
+
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Create and load a `script` element
+ *
+ * This function returns an observable that will emit when the script was
+ * successfully loaded, or throw an error if it wasn't.
+ *
+ * @param src - Script URL
+ *
+ * @returns Script observable
+ */
+export function watchScript(src: string): Observable<void> {
+ const script = h("script", { src })
+ return defer(() => {
+ document.head.appendChild(script)
+ return merge(
+ fromEvent(script, "load"),
+ fromEvent(script, "error")
+ .pipe(
+ switchMap(() => (
+ throwError(() => new ReferenceError(`Invalid script: ${src}`))
+ ))
+ )
+ )
+ .pipe(
+ map(() => undefined),
+ finalize(() => document.head.removeChild(script)),
+ take(1)
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/browser/toggle/index.ts b/src/templates/assets/javascripts/browser/toggle/index.ts
new file mode 100644
index 00000000..0be4b29d
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/toggle/index.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ fromEvent,
+ map,
+ startWith
+} from "rxjs"
+
+import { getElement } from "../element"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Toggle
+ */
+export type Toggle =
+ | "drawer" /* Toggle for drawer */
+ | "search" /* Toggle for search */
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Toggle map
+ */
+const toggles: Record<Toggle, HTMLInputElement> = {
+ drawer: getElement("[data-md-toggle=drawer]"),
+ search: getElement("[data-md-toggle=search]")
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve the value of a toggle
+ *
+ * @param name - Toggle
+ *
+ * @returns Toggle value
+ */
+export function getToggle(name: Toggle): boolean {
+ return toggles[name].checked
+}
+
+/**
+ * Set toggle
+ *
+ * Simulating a click event seems to be the most cross-browser compatible way
+ * of changing the value while also emitting a `change` event. Before, Material
+ * used `CustomEvent` to programmatically change the value of a toggle, but this
+ * is a much simpler and cleaner solution which doesn't require a polyfill.
+ *
+ * @param name - Toggle
+ * @param value - Toggle value
+ */
+export function setToggle(name: Toggle, value: boolean): void {
+ if (toggles[name].checked !== value)
+ toggles[name].click()
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Watch toggle
+ *
+ * @param name - Toggle
+ *
+ * @returns Toggle value observable
+ */
+export function watchToggle(name: Toggle): Observable<boolean> {
+ const el = toggles[name]
+ return fromEvent(el, "change")
+ .pipe(
+ map(() => el.checked),
+ startWith(el.checked)
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/viewport/_/index.ts b/src/templates/assets/javascripts/browser/viewport/_/index.ts
new file mode 100644
index 00000000..09c45f32
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/viewport/_/index.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ combineLatest,
+ map,
+ shareReplay
+} from "rxjs"
+
+import {
+ ViewportOffset,
+ watchViewportOffset
+} from "../offset"
+import {
+ ViewportSize,
+ watchViewportSize
+} from "../size"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Viewport
+ */
+export interface Viewport {
+ offset: ViewportOffset /* Viewport offset */
+ size: ViewportSize /* Viewport size */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch viewport
+ *
+ * @returns Viewport observable
+ */
+export function watchViewport(): Observable<Viewport> {
+ return combineLatest([
+ watchViewportOffset(),
+ watchViewportSize()
+ ])
+ .pipe(
+ map(([offset, size]) => ({ offset, size })),
+ shareReplay(1)
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/viewport/at/index.ts b/src/templates/assets/javascripts/browser/viewport/at/index.ts
new file mode 100644
index 00000000..8769cf3b
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/viewport/at/index.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ combineLatest,
+ distinctUntilKeyChanged,
+ map
+} from "rxjs"
+
+import { Header } from "~/components"
+
+import { getElementOffset } from "../../element"
+import { Viewport } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch viewport relative to element
+ *
+ * @param el - Element
+ * @param options - Options
+ *
+ * @returns Viewport observable
+ */
+export function watchViewportAt(
+ el: HTMLElement, { viewport$, header$ }: WatchOptions
+): Observable<Viewport> {
+ const size$ = viewport$
+ .pipe(
+ distinctUntilKeyChanged("size")
+ )
+
+ /* Compute element offset */
+ const offset$ = combineLatest([size$, header$])
+ .pipe(
+ map(() => getElementOffset(el))
+ )
+
+ /* Compute relative viewport, return hot observable */
+ return combineLatest([header$, viewport$, offset$])
+ .pipe(
+ map(([{ height }, { offset, size }, { x, y }]) => ({
+ offset: {
+ x: offset.x - x,
+ y: offset.y - y + height
+ },
+ size
+ }))
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/viewport/index.ts b/src/templates/assets/javascripts/browser/viewport/index.ts
new file mode 100644
index 00000000..b3d135e9
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/viewport/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./at"
+export * from "./offset"
+export * from "./size"
diff --git a/src/templates/assets/javascripts/browser/viewport/offset/index.ts b/src/templates/assets/javascripts/browser/viewport/offset/index.ts
new file mode 100644
index 00000000..63d37dd2
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/viewport/offset/index.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ fromEvent,
+ map,
+ merge,
+ startWith
+} from "rxjs"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Viewport offset
+ */
+export interface ViewportOffset {
+ x: number /* Horizontal offset */
+ y: number /* Vertical offset */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve viewport offset
+ *
+ * On iOS Safari, viewport offset can be negative due to overflow scrolling.
+ * As this may induce strange behaviors downstream, we'll just limit it to 0.
+ *
+ * @returns Viewport offset
+ */
+export function getViewportOffset(): ViewportOffset {
+ return {
+ x: Math.max(0, scrollX),
+ y: Math.max(0, scrollY)
+ }
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Watch viewport offset
+ *
+ * @returns Viewport offset observable
+ */
+export function watchViewportOffset(): Observable<ViewportOffset> {
+ return merge(
+ fromEvent(window, "scroll", { passive: true }),
+ fromEvent(window, "resize", { passive: true })
+ )
+ .pipe(
+ map(getViewportOffset),
+ startWith(getViewportOffset())
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/viewport/size/index.ts b/src/templates/assets/javascripts/browser/viewport/size/index.ts
new file mode 100644
index 00000000..06694888
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/viewport/size/index.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ fromEvent,
+ map,
+ startWith
+} from "rxjs"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Viewport size
+ */
+export interface ViewportSize {
+ width: number /* Viewport width */
+ height: number /* Viewport height */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve viewport size
+ *
+ * @returns Viewport size
+ */
+export function getViewportSize(): ViewportSize {
+ return {
+ width: innerWidth,
+ height: innerHeight
+ }
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Watch viewport size
+ *
+ * @returns Viewport size observable
+ */
+export function watchViewportSize(): Observable<ViewportSize> {
+ return fromEvent(window, "resize", { passive: true })
+ .pipe(
+ map(getViewportSize),
+ startWith(getViewportSize())
+ )
+}
diff --git a/src/templates/assets/javascripts/browser/worker/index.ts b/src/templates/assets/javascripts/browser/worker/index.ts
new file mode 100644
index 00000000..12e4e63b
--- /dev/null
+++ b/src/templates/assets/javascripts/browser/worker/index.ts
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ endWith,
+ fromEvent,
+ ignoreElements,
+ mergeWith,
+ share,
+ takeUntil
+} from "rxjs"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Worker message
+ */
+export interface WorkerMessage {
+ type: unknown /* Message type */
+ data?: unknown /* Message data */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Create an observable for receiving from a web worker
+ *
+ * @template T - Data type
+ *
+ * @param worker - Web worker
+ *
+ * @returns Message observable
+ */
+function recv<T>(worker: Worker): Observable<T> {
+ return fromEvent<MessageEvent<T>, T>(worker, "message", ev => ev.data)
+}
+
+/**
+ * Create a subject for sending to a web worker
+ *
+ * @template T - Data type
+ *
+ * @param worker - Web worker
+ *
+ * @returns Message subject
+ */
+function send<T>(worker: Worker): Subject<T> {
+ const send$ = new Subject<T>()
+ send$.subscribe(data => worker.postMessage(data))
+
+ /* Return message subject */
+ return send$
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Create a bidirectional communication channel to a web worker
+ *
+ * @template T - Data type
+ *
+ * @param url - Worker URL
+ * @param worker - Worker
+ *
+ * @returns Worker subject
+ */
+export function watchWorker<T extends WorkerMessage>(
+ url: string, worker = new Worker(url)
+): Subject<T> {
+ const recv$ = recv<T>(worker)
+ const send$ = send<T>(worker)
+
+ /* Create worker subject and forward messages */
+ const worker$ = new Subject<T>()
+ worker$.subscribe(send$)
+
+ /* Return worker subject */
+ const done$ = send$.pipe(ignoreElements(), endWith(true))
+ return worker$
+ .pipe(
+ ignoreElements(),
+ mergeWith(recv$.pipe(takeUntil(done$))),
+ share()
+ ) as Subject<T>
+}
diff --git a/src/templates/assets/javascripts/bundle.ts b/src/templates/assets/javascripts/bundle.ts
new file mode 100644
index 00000000..141789c9
--- /dev/null
+++ b/src/templates/assets/javascripts/bundle.ts
@@ -0,0 +1,316 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import "focus-visible"
+
+import {
+ EMPTY,
+ NEVER,
+ Observable,
+ Subject,
+ defer,
+ delay,
+ filter,
+ map,
+ merge,
+ mergeWith,
+ shareReplay,
+ switchMap
+} from "rxjs"
+
+import { configuration, feature } from "./_"
+import {
+ at,
+ getActiveElement,
+ getOptionalElement,
+ requestJSON,
+ setLocation,
+ setToggle,
+ watchDocument,
+ watchKeyboard,
+ watchLocation,
+ watchLocationTarget,
+ watchMedia,
+ watchPrint,
+ watchScript,
+ watchViewport
+} from "./browser"
+import {
+ getComponentElement,
+ getComponentElements,
+ mountAnnounce,
+ mountBackToTop,
+ mountConsent,
+ mountContent,
+ mountDialog,
+ mountHeader,
+ mountHeaderTitle,
+ mountPalette,
+ mountProgress,
+ mountSearch,
+ mountSearchHiglight,
+ mountSidebar,
+ mountSource,
+ mountTableOfContents,
+ mountTabs,
+ watchHeader,
+ watchMain
+} from "./components"
+import {
+ SearchIndex,
+ setupClipboardJS,
+ setupInstantNavigation,
+ setupVersionSelector
+} from "./integrations"
+import {
+ patchIndeterminate,
+ patchScrollfix,
+ patchScrolllock
+} from "./patches"
+import "./polyfills"
+
+/* ----------------------------------------------------------------------------
+ * Functions - @todo refactor
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Fetch search index
+ *
+ * @returns Search index observable
+ */
+function fetchSearchIndex(): Observable<SearchIndex> {
+ if (location.protocol === "file:") {
+ return watchScript(
+ `${new URL("search/search_index.js", config.base)}`
+ )
+ .pipe(
+ // @ts-ignore - @todo fix typings
+ map(() => __index),
+ shareReplay(1)
+ )
+ } else {
+ return requestJSON<SearchIndex>(
+ new URL("search/search_index.json", config.base)
+ )
+ }
+}
+
+/* ----------------------------------------------------------------------------
+ * Application
+ * ------------------------------------------------------------------------- */
+
+/* Yay, JavaScript is available */
+document.documentElement.classList.remove("no-js")
+document.documentElement.classList.add("js")
+
+/* Set up navigation observables and subjects */
+const document$ = watchDocument()
+const location$ = watchLocation()
+const target$ = watchLocationTarget(location$)
+const keyboard$ = watchKeyboard()
+
+/* Set up media observables */
+const viewport$ = watchViewport()
+const tablet$ = watchMedia("(min-width: 960px)")
+const screen$ = watchMedia("(min-width: 1220px)")
+const print$ = watchPrint()
+
+/* Retrieve search index, if search is enabled */
+const config = configuration()
+const index$ = document.forms.namedItem("search")
+ ? fetchSearchIndex()
+ : NEVER
+
+/* Set up Clipboard.js integration */
+const alert$ = new Subject<string>()
+setupClipboardJS({ alert$ })
+
+/* Set up progress indicator */
+const progress$ = new Subject<number>()
+
+/* Set up instant navigation, if enabled */
+if (feature("navigation.instant"))
+ setupInstantNavigation({ location$, viewport$, progress$ })
+ .subscribe(document$)
+
+/* Set up version selector */
+if (config.version?.provider === "mike")
+ setupVersionSelector({ document$ })
+
+/* Always close drawer and search on navigation */
+merge(location$, target$)
+ .pipe(
+ delay(125)
+ )
+ .subscribe(() => {
+ setToggle("drawer", false)
+ setToggle("search", false)
+ })
+
+/* Set up global keyboard handlers */
+keyboard$
+ .pipe(
+ filter(({ mode }) => mode === "global")
+ )
+ .subscribe(key => {
+ switch (key.type) {
+
+ /* Go to previous page */
+ case "p":
+ case ",":
+ const prev = getOptionalElement<HTMLLinkElement>("link[rel=prev]")
+ if (typeof prev !== "undefined")
+ setLocation(prev)
+ break
+
+ /* Go to next page */
+ case "n":
+ case ".":
+ const next = getOptionalElement<HTMLLinkElement>("link[rel=next]")
+ if (typeof next !== "undefined")
+ setLocation(next)
+ break
+
+ /* Expand navigation, see https://bit.ly/3ZjG5io */
+ case "Enter":
+ const active = getActiveElement()
+ if (active instanceof HTMLLabelElement)
+ active.click()
+ }
+ })
+
+/* Set up patches */
+patchIndeterminate({ document$, tablet$ })
+patchScrollfix({ document$ })
+patchScrolllock({ viewport$, tablet$ })
+
+/* Set up header and main area observable */
+const header$ = watchHeader(getComponentElement("header"), { viewport$ })
+const main$ = document$
+ .pipe(
+ map(() => getComponentElement("main")),
+ switchMap(el => watchMain(el, { viewport$, header$ })),
+ shareReplay(1)
+ )
+
+/* Set up control component observables */
+const control$ = merge(
+
+ /* Consent */
+ ...getComponentElements("consent")
+ .map(el => mountConsent(el, { target$ })),
+
+ /* Dialog */
+ ...getComponentElements("dialog")
+ .map(el => mountDialog(el, { alert$ })),
+
+ /* Header */
+ ...getComponentElements("header")
+ .map(el => mountHeader(el, { viewport$, header$, main$ })),
+
+ /* Color palette */
+ ...getComponentElements("palette")
+ .map(el => mountPalette(el)),
+
+ /* Progress bar */
+ ...getComponentElements("progress")
+ .map(el => mountProgress(el, { progress$ })),
+
+ /* Search */
+ ...getComponentElements("search")
+ .map(el => mountSearch(el, { index$, keyboard$ })),
+
+ /* Repository information */
+ ...getComponentElements("source")
+ .map(el => mountSource(el))
+)
+
+/* Set up content component observables */
+const content$ = defer(() => merge(
+
+ /* Announcement bar */
+ ...getComponentElements("announce")
+ .map(el => mountAnnounce(el)),
+
+ /* Content */
+ ...getComponentElements("content")
+ .map(el => mountContent(el, { viewport$, target$, print$ })),
+
+ /* Search highlighting */
+ ...getComponentElements("content")
+ .map(el => feature("search.highlight")
+ ? mountSearchHiglight(el, { index$, location$ })
+ : EMPTY
+ ),
+
+ /* Header title */
+ ...getComponentElements("header-title")
+ .map(el => mountHeaderTitle(el, { viewport$, header$ })),
+
+ /* Sidebar */
+ ...getComponentElements("sidebar")
+ .map(el => el.getAttribute("data-md-type") === "navigation"
+ ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))
+ : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))
+ ),
+
+ /* Navigation tabs */
+ ...getComponentElements("tabs")
+ .map(el => mountTabs(el, { viewport$, header$ })),
+
+ /* Table of contents */
+ ...getComponentElements("toc")
+ .map(el => mountTableOfContents(el, {
+ viewport$, header$, main$, target$
+ })),
+
+ /* Back-to-top button */
+ ...getComponentElements("top")
+ .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))
+))
+
+/* Set up component observables */
+const component$ = document$
+ .pipe(
+ switchMap(() => content$),
+ mergeWith(control$),
+ shareReplay(1)
+ )
+
+/* Subscribe to all components */
+component$.subscribe()
+
+/* ----------------------------------------------------------------------------
+ * Exports
+ * ------------------------------------------------------------------------- */
+
+window.document$ = document$ /* Document observable */
+window.location$ = location$ /* Location subject */
+window.target$ = target$ /* Location target observable */
+window.keyboard$ = keyboard$ /* Keyboard observable */
+window.viewport$ = viewport$ /* Viewport observable */
+window.tablet$ = tablet$ /* Media tablet observable */
+window.screen$ = screen$ /* Media screen observable */
+window.print$ = print$ /* Media print observable */
+window.alert$ = alert$ /* Alert subject */
+window.progress$ = progress$ /* Progress indicator subject */
+window.component$ = component$ /* Component observable */
diff --git a/src/templates/assets/javascripts/components/_/index.ts b/src/templates/assets/javascripts/components/_/index.ts
new file mode 100644
index 00000000..61c471d9
--- /dev/null
+++ b/src/templates/assets/javascripts/components/_/index.ts
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { getElement, getElements } from "~/browser"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Component type
+ */
+export type ComponentType =
+ | "announce" /* Announcement bar */
+ | "container" /* Container */
+ | "consent" /* Consent */
+ | "content" /* Content */
+ | "dialog" /* Dialog */
+ | "header" /* Header */
+ | "header-title" /* Header title */
+ | "header-topic" /* Header topic */
+ | "main" /* Main area */
+ | "outdated" /* Version warning */
+ | "palette" /* Color palette */
+ | "progress" /* Progress indicator */
+ | "search" /* Search */
+ | "search-query" /* Search input */
+ | "search-result" /* Search results */
+ | "search-share" /* Search sharing */
+ | "search-suggest" /* Search suggestions */
+ | "sidebar" /* Sidebar */
+ | "skip" /* Skip link */
+ | "source" /* Repository information */
+ | "tabs" /* Navigation tabs */
+ | "toc" /* Table of contents */
+ | "top" /* Back-to-top button */
+
+/**
+ * Component
+ *
+ * @template T - Component type
+ * @template U - Reference type
+ */
+export type Component<
+ T extends {} = {},
+ U extends HTMLElement = HTMLElement
+> =
+ T & {
+ ref: U /* Component reference */
+ }
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Component type map
+ */
+interface ComponentTypeMap {
+ "announce": HTMLElement /* Announcement bar */
+ "container": HTMLElement /* Container */
+ "consent": HTMLElement /* Consent */
+ "content": HTMLElement /* Content */
+ "dialog": HTMLElement /* Dialog */
+ "header": HTMLElement /* Header */
+ "header-title": HTMLElement /* Header title */
+ "header-topic": HTMLElement /* Header topic */
+ "main": HTMLElement /* Main area */
+ "outdated": HTMLElement /* Version warning */
+ "palette": HTMLElement /* Color palette */
+ "progress": HTMLElement /* Progress indicator */
+ "search": HTMLElement /* Search */
+ "search-query": HTMLInputElement /* Search input */
+ "search-result": HTMLElement /* Search results */
+ "search-share": HTMLAnchorElement /* Search sharing */
+ "search-suggest": HTMLElement /* Search suggestions */
+ "sidebar": HTMLElement /* Sidebar */
+ "skip": HTMLAnchorElement /* Skip link */
+ "source": HTMLAnchorElement /* Repository information */
+ "tabs": HTMLElement /* Navigation tabs */
+ "toc": HTMLElement /* Table of contents */
+ "top": HTMLAnchorElement /* Back-to-top button */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Retrieve the element for a given component or throw a reference error
+ *
+ * @template T - Component type
+ *
+ * @param type - Component type
+ * @param node - Node of reference
+ *
+ * @returns Element
+ */
+export function getComponentElement<T extends ComponentType>(
+ type: T, node: ParentNode = document
+): ComponentTypeMap[T] {
+ return getElement(`[data-md-component=${type}]`, node)
+}
+
+/**
+ * Retrieve all elements for a given component
+ *
+ * @template T - Component type
+ *
+ * @param type - Component type
+ * @param node - Node of reference
+ *
+ * @returns Elements
+ */
+export function getComponentElements<T extends ComponentType>(
+ type: T, node: ParentNode = document
+): ComponentTypeMap[T][] {
+ return getElements(`[data-md-component=${type}]`, node)
+}
diff --git a/src/templates/assets/javascripts/components/announce/index.ts b/src/templates/assets/javascripts/components/announce/index.ts
new file mode 100644
index 00000000..dd04b4ff
--- /dev/null
+++ b/src/templates/assets/javascripts/components/announce/index.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Observable,
+ Subject,
+ defer,
+ finalize,
+ fromEvent,
+ map,
+ tap
+} from "rxjs"
+
+import { feature } from "~/_"
+import { getElement } from "~/browser"
+
+import { Component } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Announcement bar
+ */
+export interface Announce {
+ hash: number /* Content hash */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch announcement bar
+ *
+ * @param el - Announcement bar element
+ *
+ * @returns Announcement bar observable
+ */
+export function watchAnnounce(
+ el: HTMLElement
+): Observable<Announce> {
+ const button = getElement(".md-typeset > :first-child", el)
+ return fromEvent(button, "click", { once: true })
+ .pipe(
+ map(() => getElement(".md-typeset", el)),
+ map(content => ({ hash: __md_hash(content.innerHTML) }))
+ )
+}
+
+/**
+ * Mount announcement bar
+ *
+ * @param el - Announcement bar element
+ *
+ * @returns Announcement bar component observable
+ */
+export function mountAnnounce(
+ el: HTMLElement
+): Observable<Component<Announce>> {
+ if (!feature("announce.dismiss") || !el.childElementCount)
+ return EMPTY
+
+ /* Support instant navigation - see https://t.ly/3FTme */
+ if (!el.hidden) {
+ const content = getElement(".md-typeset", el)
+ if (__md_hash(content.innerHTML) === __md_get("__announce"))
+ el.hidden = true
+ }
+
+ /* Mount component on subscription */
+ return defer(() => {
+ const push$ = new Subject<Announce>()
+ push$.subscribe(({ hash }) => {
+ el.hidden = true
+
+ /* Persist preference in local storage */
+ __md_set<number>("__announce", hash)
+ })
+
+ /* Create and return component */
+ return watchAnnounce(el)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/consent/index.ts b/src/templates/assets/javascripts/components/consent/index.ts
new file mode 100644
index 00000000..bc99db58
--- /dev/null
+++ b/src/templates/assets/javascripts/components/consent/index.ts
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ finalize,
+ map,
+ tap
+} from "rxjs"
+
+import { Component } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Consent
+ */
+export interface Consent {
+ hidden: boolean /* Consent is hidden */
+}
+
+/**
+ * Consent defaults
+ */
+export interface ConsentDefaults {
+ analytics?: boolean /* Consent for Analytics */
+ github?: boolean /* Consent for GitHub */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ target$: Observable<HTMLElement> /* Target observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ target$: Observable<HTMLElement> /* Target observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch consent
+ *
+ * @param el - Consent element
+ * @param options - Options
+ *
+ * @returns Consent observable
+ */
+export function watchConsent(
+ el: HTMLElement, { target$ }: WatchOptions
+): Observable<Consent> {
+ return target$
+ .pipe(
+ map(target => ({ hidden: target !== el }))
+ )
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Mount consent
+ *
+ * @param el - Consent element
+ * @param options - Options
+ *
+ * @returns Consent component observable
+ */
+export function mountConsent(
+ el: HTMLElement, options: MountOptions
+): Observable<Component<Consent>> {
+ const internal$ = new Subject<Consent>()
+ internal$.subscribe(({ hidden }) => {
+ el.hidden = hidden
+ })
+
+ /* Create and return component */
+ return watchConsent(el, options)
+ .pipe(
+ tap(state => internal$.next(state)),
+ finalize(() => internal$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+}
diff --git a/src/templates/assets/javascripts/components/content/_/index.ts b/src/templates/assets/javascripts/components/content/_/index.ts
new file mode 100644
index 00000000..899a695c
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/_/index.ts
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { Observable, merge } from "rxjs"
+
+import { Viewport, getElements } from "~/browser"
+
+import { Component } from "../../_"
+import {
+ Annotation,
+ mountAnnotationBlock
+} from "../annotation"
+import {
+ CodeBlock,
+ mountCodeBlock
+} from "../code"
+import {
+ Details,
+ mountDetails
+} from "../details"
+import {
+ Mermaid,
+ mountMermaid
+} from "../mermaid"
+import {
+ DataTable,
+ mountDataTable
+} from "../table"
+import {
+ ContentTabs,
+ mountContentTabs
+} from "../tabs"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Content
+ */
+export type Content =
+ | Annotation
+ | CodeBlock
+ | ContentTabs
+ | DataTable
+ | Details
+ | Mermaid
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ target$: Observable<HTMLElement> /* Location target observable */
+ print$: Observable<boolean> /* Media print observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount content
+ *
+ * This function mounts all components that are found in the content of the
+ * actual article, including code blocks, data tables and details.
+ *
+ * @param el - Content element
+ * @param options - Options
+ *
+ * @returns Content component observable
+ */
+export function mountContent(
+ el: HTMLElement, { viewport$, target$, print$ }: MountOptions
+): Observable<Component<Content>> {
+ return merge(
+
+ /* Annotations */
+ ...getElements(".annotate:not(.highlight)", el)
+ .map(child => mountAnnotationBlock(child, { target$, print$ })),
+
+ /* Code blocks */
+ ...getElements("pre:not(.mermaid) > code", el)
+ .map(child => mountCodeBlock(child, { target$, print$ })),
+
+ /* Mermaid diagrams */
+ ...getElements("pre.mermaid", el)
+ .map(child => mountMermaid(child)),
+
+ /* Data tables */
+ ...getElements("table:not([class])", el)
+ .map(child => mountDataTable(child)),
+
+ /* Details */
+ ...getElements("details", el)
+ .map(child => mountDetails(child, { target$, print$ })),
+
+ /* Content tabs */
+ ...getElements("[data-tabs]", el)
+ .map(child => mountContentTabs(child, { viewport$ }))
+ )
+}
diff --git a/src/templates/assets/javascripts/components/content/annotation/_/index.ts b/src/templates/assets/javascripts/components/content/annotation/_/index.ts
new file mode 100644
index 00000000..c5138fa4
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/annotation/_/index.ts
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ animationFrameScheduler,
+ auditTime,
+ combineLatest,
+ debounceTime,
+ defer,
+ delay,
+ endWith,
+ filter,
+ finalize,
+ fromEvent,
+ ignoreElements,
+ map,
+ merge,
+ switchMap,
+ take,
+ takeUntil,
+ tap,
+ throttleTime,
+ withLatestFrom
+} from "rxjs"
+
+import {
+ ElementOffset,
+ getActiveElement,
+ getElementSize,
+ watchElementContentOffset,
+ watchElementFocus,
+ watchElementOffset,
+ watchElementVisibility
+} from "~/browser"
+
+import { Component } from "../../../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Annotation
+ */
+export interface Annotation {
+ active: boolean /* Annotation is active */
+ offset: ElementOffset /* Annotation offset */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ target$: Observable<HTMLElement> /* Location target observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch annotation
+ *
+ * @param el - Annotation element
+ * @param container - Containing element
+ *
+ * @returns Annotation observable
+ */
+export function watchAnnotation(
+ el: HTMLElement, container: HTMLElement
+): Observable<Annotation> {
+ const offset$ = defer(() => combineLatest([
+ watchElementOffset(el),
+ watchElementContentOffset(container)
+ ]))
+ .pipe(
+ map(([{ x, y }, scroll]): ElementOffset => {
+ const { width, height } = getElementSize(el)
+ return ({
+ x: x - scroll.x + width / 2,
+ y: y - scroll.y + height / 2
+ })
+ })
+ )
+
+ /* Actively watch annotation on focus */
+ return watchElementFocus(el)
+ .pipe(
+ switchMap(active => offset$
+ .pipe(
+ map(offset => ({ active, offset })),
+ take(+!active || Infinity)
+ )
+ )
+ )
+}
+
+/**
+ * Mount annotation
+ *
+ * @param el - Annotation element
+ * @param container - Containing element
+ * @param options - Options
+ *
+ * @returns Annotation component observable
+ */
+export function mountAnnotation(
+ el: HTMLElement, container: HTMLElement, { target$ }: MountOptions
+): Observable<Component<Annotation>> {
+ const [tooltip, index] = Array.from(el.children)
+
+ /* Mount component on subscription */
+ return defer(() => {
+ const push$ = new Subject<Annotation>()
+ const done$ = push$.pipe(ignoreElements(), endWith(true))
+ push$.subscribe({
+
+ /* Handle emission */
+ next({ offset }) {
+ el.style.setProperty("--md-tooltip-x", `${offset.x}px`)
+ el.style.setProperty("--md-tooltip-y", `${offset.y}px`)
+ },
+
+ /* Handle complete */
+ complete() {
+ el.style.removeProperty("--md-tooltip-x")
+ el.style.removeProperty("--md-tooltip-y")
+ }
+ })
+
+ /* Start animation only when annotation is visible */
+ watchElementVisibility(el)
+ .pipe(
+ takeUntil(done$)
+ )
+ .subscribe(visible => {
+ el.toggleAttribute("data-md-visible", visible)
+ })
+
+ /* Toggle tooltip presence to mitigate empty lines when copying */
+ merge(
+ push$.pipe(filter(({ active }) => active)),
+ push$.pipe(debounceTime(250), filter(({ active }) => !active))
+ )
+ .subscribe({
+
+ /* Handle emission */
+ next({ active }) {
+ if (active)
+ el.prepend(tooltip)
+ else
+ tooltip.remove()
+ },
+
+ /* Handle complete */
+ complete() {
+ el.prepend(tooltip)
+ }
+ })
+
+ /* Toggle tooltip visibility */
+ push$
+ .pipe(
+ auditTime(16, animationFrameScheduler)
+ )
+ .subscribe(({ active }) => {
+ tooltip.classList.toggle("md-tooltip--active", active)
+ })
+
+ /* Track relative origin of tooltip */
+ push$
+ .pipe(
+ throttleTime(125, animationFrameScheduler),
+ filter(() => !!el.offsetParent),
+ map(() => el.offsetParent!.getBoundingClientRect()),
+ map(({ x }) => x)
+ )
+ .subscribe({
+
+ /* Handle emission */
+ next(origin) {
+ if (origin)
+ el.style.setProperty("--md-tooltip-0", `${-origin}px`)
+ else
+ el.style.removeProperty("--md-tooltip-0")
+ },
+
+ /* Handle complete */
+ complete() {
+ el.style.removeProperty("--md-tooltip-0")
+ }
+ })
+
+ /* Allow to copy link without scrolling to anchor */
+ fromEvent<MouseEvent>(index, "click")
+ .pipe(
+ takeUntil(done$),
+ filter(ev => !(ev.metaKey || ev.ctrlKey))
+ )
+ .subscribe(ev => {
+ ev.stopPropagation()
+ ev.preventDefault()
+ })
+
+ /* Allow to open link in new tab or blur on close */
+ fromEvent<MouseEvent>(index, "mousedown")
+ .pipe(
+ takeUntil(done$),
+ withLatestFrom(push$)
+ )
+ .subscribe(([ev, { active }]) => {
+
+ /* Open in new tab */
+ if (ev.button !== 0 || ev.metaKey || ev.ctrlKey) {
+ ev.preventDefault()
+
+ /* Close annotation */
+ } else if (active) {
+ ev.preventDefault()
+
+ /* Focus parent annotation, if any */
+ const parent = el.parentElement!.closest(".md-annotation")
+ if (parent instanceof HTMLElement)
+ parent.focus()
+ else
+ getActiveElement()?.blur()
+ }
+ })
+
+ /* Open and focus annotation on location target */
+ target$
+ .pipe(
+ takeUntil(done$),
+ filter(target => target === tooltip),
+ delay(125)
+ )
+ .subscribe(() => el.focus())
+
+ /* Create and return component */
+ return watchAnnotation(el, container)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/content/annotation/block/index.ts b/src/templates/assets/javascripts/components/content/annotation/block/index.ts
new file mode 100644
index 00000000..c73b01fa
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/annotation/block/index.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { EMPTY, Observable, defer } from "rxjs"
+
+import { Component } from "../../../_"
+import { Annotation } from "../_"
+import { mountAnnotationList } from "../list"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ target$: Observable<HTMLElement> /* Location target observable */
+ print$: Observable<boolean> /* Media print observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Find list element directly following a block
+ *
+ * @param el - Annotation block element
+ *
+ * @returns List element or nothing
+ */
+function findList(el: HTMLElement): HTMLElement | undefined {
+ if (el.nextElementSibling) {
+ const sibling = el.nextElementSibling as HTMLElement
+ if (sibling.tagName === "OL")
+ return sibling
+
+ /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */
+ else if (sibling.tagName === "P" && !sibling.children.length)
+ return findList(sibling)
+ }
+
+ /* Everything else */
+ return undefined
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount annotation block
+ *
+ * @param el - Annotation block element
+ * @param options - Options
+ *
+ * @returns Annotation component observable
+ */
+export function mountAnnotationBlock(
+ el: HTMLElement, options: MountOptions
+): Observable<Component<Annotation>> {
+ return defer(() => {
+ const list = findList(el)
+ return typeof list !== "undefined"
+ ? mountAnnotationList(list, el, options)
+ : EMPTY
+ })
+}
diff --git a/src/templates/assets/javascripts/components/content/annotation/index.ts b/src/templates/assets/javascripts/components/content/annotation/index.ts
new file mode 100644
index 00000000..c593b723
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/annotation/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./block"
+export * from "./list"
diff --git a/src/templates/assets/javascripts/components/content/annotation/list/index.ts b/src/templates/assets/javascripts/components/content/annotation/list/index.ts
new file mode 100644
index 00000000..725dd583
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/annotation/list/index.ts
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Observable,
+ Subject,
+ defer,
+ endWith,
+ finalize,
+ ignoreElements,
+ merge,
+ share,
+ takeUntil
+} from "rxjs"
+
+import {
+ getElement,
+ getElements,
+ getOptionalElement
+} from "~/browser"
+import { renderAnnotation } from "~/templates"
+
+import { Component } from "../../../_"
+import {
+ Annotation,
+ mountAnnotation
+} from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ target$: Observable<HTMLElement> /* Location target observable */
+ print$: Observable<boolean> /* Media print observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Find all annotation hosts in the containing element
+ *
+ * @param container - Containing element
+ *
+ * @returns Annotation hosts
+ */
+function findHosts(container: HTMLElement): HTMLElement[] {
+ return container.tagName === "CODE"
+ ? getElements(".c, .c1, .cm", container)
+ : [container]
+}
+
+/**
+ * Find all annotation markers in the containing element
+ *
+ * @param container - Containing element
+ *
+ * @returns Annotation markers
+ */
+function findMarkers(container: HTMLElement): Text[] {
+ const markers: Text[] = []
+ for (const el of findHosts(container)) {
+ const nodes: Text[] = []
+
+ /* Find all text nodes in current element */
+ const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT)
+ for (let node = it.nextNode(); node; node = it.nextNode())
+ nodes.push(node as Text)
+
+ /* Find all markers in each text node */
+ for (let text of nodes) {
+ let match: RegExpExecArray | null
+
+ /* Split text at marker and add to list */
+ while ((match = /(\(\d+\))(!)?/.exec(text.textContent!))) {
+ const [, id, force] = match
+ if (typeof force === "undefined") {
+ const marker = text.splitText(match.index)
+ text = marker.splitText(id.length)
+ markers.push(marker)
+
+ /* Replace entire text with marker */
+ } else {
+ text.textContent = id
+ markers.push(text)
+ break
+ }
+ }
+ }
+ }
+ return markers
+}
+
+/**
+ * Swap the child nodes of two elements
+ *
+ * @param source - Source element
+ * @param target - Target element
+ */
+function swap(source: HTMLElement, target: HTMLElement): void {
+ target.append(...Array.from(source.childNodes))
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount annotation list
+ *
+ * This function analyzes the containing code block and checks for markers
+ * referring to elements in the given annotation list. If no markers are found,
+ * the list is left untouched. Otherwise, list elements are rendered as
+ * annotations inside the code block.
+ *
+ * @param el - Annotation list element
+ * @param container - Containing element
+ * @param options - Options
+ *
+ * @returns Annotation component observable
+ */
+export function mountAnnotationList(
+ el: HTMLElement, container: HTMLElement, { target$, print$ }: MountOptions
+): Observable<Component<Annotation>> {
+
+ /* Compute prefix for tooltip anchors */
+ const parent = container.closest("[id]")
+ const prefix = parent?.id
+
+ /* Find and replace all markers with empty annotations */
+ const annotations = new Map<string, HTMLElement>()
+ for (const marker of findMarkers(container)) {
+ const [, id] = marker.textContent!.match(/\((\d+)\)/)!
+ if (getOptionalElement(`:scope > li:nth-child(${id})`, el)) {
+ annotations.set(id, renderAnnotation(id, prefix))
+ marker.replaceWith(annotations.get(id)!)
+ }
+ }
+
+ /* Keep list if there are no annotations to render */
+ if (annotations.size === 0)
+ return EMPTY
+
+ /* Mount component on subscription */
+ return defer(() => {
+ const push$ = new Subject()
+ const done$ = push$.pipe(ignoreElements(), endWith(true))
+
+ /* Retrieve container pairs for swapping */
+ const pairs: [HTMLElement, HTMLElement][] = []
+ for (const [id, annotation] of annotations)
+ pairs.push([
+ getElement(".md-typeset", annotation),
+ getElement(`:scope > li:nth-child(${id})`, el)
+ ])
+
+ /* Handle print mode - see https://bit.ly/3rgPdpt */
+ print$.pipe(takeUntil(done$))
+ .subscribe(active => {
+ el.hidden = !active
+
+ /* Add class to discern list element */
+ el.classList.toggle("md-annotation-list", active)
+
+ /* Show annotations in code block or list (print) */
+ for (const [inner, child] of pairs)
+ if (!active)
+ swap(child, inner)
+ else
+ swap(inner, child)
+ })
+
+ /* Create and return component */
+ return merge(...[...annotations]
+ .map(([, annotation]) => (
+ mountAnnotation(annotation, container, { target$ })
+ ))
+ )
+ .pipe(
+ finalize(() => push$.complete()),
+ share()
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/content/code/_/index.ts b/src/templates/assets/javascripts/components/content/code/_/index.ts
new file mode 100644
index 00000000..ccc09339
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/code/_/index.ts
@@ -0,0 +1,238 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import ClipboardJS from "clipboard"
+import {
+ EMPTY,
+ Observable,
+ Subject,
+ defer,
+ distinctUntilChanged,
+ distinctUntilKeyChanged,
+ filter,
+ finalize,
+ map,
+ mergeWith,
+ switchMap,
+ take,
+ tap
+} from "rxjs"
+
+import { feature } from "~/_"
+import {
+ getElementContentSize,
+ watchElementSize,
+ watchElementVisibility
+} from "~/browser"
+import { renderClipboardButton } from "~/templates"
+
+import { Component } from "../../../_"
+import {
+ Annotation,
+ mountAnnotationList
+} from "../../annotation"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Code block
+ */
+export interface CodeBlock {
+ scrollable: boolean /* Code block overflows */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ target$: Observable<HTMLElement> /* Location target observable */
+ print$: Observable<boolean> /* Media print observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Global sequence number for code blocks
+ */
+let sequence = 0
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Find candidate list element directly following a code block
+ *
+ * @param el - Code block element
+ *
+ * @returns List element or nothing
+ */
+function findCandidateList(el: HTMLElement): HTMLElement | undefined {
+ if (el.nextElementSibling) {
+ const sibling = el.nextElementSibling as HTMLElement
+ if (sibling.tagName === "OL")
+ return sibling
+
+ /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */
+ else if (sibling.tagName === "P" && !sibling.children.length)
+ return findCandidateList(sibling)
+ }
+
+ /* Everything else */
+ return undefined
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch code block
+ *
+ * This function monitors size changes of the viewport, as well as switches of
+ * content tabs with embedded code blocks, as both may trigger overflow.
+ *
+ * @param el - Code block element
+ *
+ * @returns Code block observable
+ */
+export function watchCodeBlock(
+ el: HTMLElement
+): Observable<CodeBlock> {
+ return watchElementSize(el)
+ .pipe(
+ map(({ width }) => {
+ const content = getElementContentSize(el)
+ return {
+ scrollable: content.width > width
+ }
+ }),
+ distinctUntilKeyChanged("scrollable")
+ )
+}
+
+/**
+ * Mount code block
+ *
+ * This function ensures that an overflowing code block is focusable through
+ * keyboard, so it can be scrolled without a mouse to improve on accessibility.
+ * Furthermore, if code annotations are enabled, they are mounted if and only
+ * if the code block is currently visible, e.g., not in a hidden content tab.
+ *
+ * Note that code blocks may be mounted eagerly or lazily. If they're mounted
+ * lazily (on first visibility), code annotation anchor links will not work,
+ * as they are evaluated on initial page load, and code annotations in general
+ * might feel a little bumpier.
+ *
+ * @param el - Code block element
+ * @param options - Options
+ *
+ * @returns Code block and annotation component observable
+ */
+export function mountCodeBlock(
+ el: HTMLElement, options: MountOptions
+): Observable<Component<CodeBlock | Annotation>> {
+ const { matches: hover } = matchMedia("(hover)")
+
+ /* Defer mounting of code block - see https://bit.ly/3vHVoVD */
+ const factory$ = defer(() => {
+ const push$ = new Subject<CodeBlock>()
+ push$.subscribe(({ scrollable }) => {
+ if (scrollable && hover)
+ el.setAttribute("tabindex", "0")
+ else
+ el.removeAttribute("tabindex")
+ })
+
+ /* Render button for Clipboard.js integration */
+ if (ClipboardJS.isSupported()) {
+ if (el.closest(".copy") || (
+ feature("content.code.copy") && !el.closest(".no-copy")
+ )) {
+ const parent = el.closest("pre")!
+ parent.id = `__code_${sequence++}`
+ parent.insertBefore(
+ renderClipboardButton(parent.id),
+ el
+ )
+ }
+ }
+
+ /* Handle code annotations */
+ const container = el.closest(".highlight")
+ if (container instanceof HTMLElement) {
+ const list = findCandidateList(container)
+
+ /* Mount code annotations, if enabled */
+ if (typeof list !== "undefined" && (
+ container.classList.contains("annotate") ||
+ feature("content.code.annotate")
+ )) {
+ const annotations$ = mountAnnotationList(list, el, options)
+
+ /* Create and return component */
+ return watchCodeBlock(el)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state })),
+ mergeWith(
+ watchElementSize(container)
+ .pipe(
+ map(({ width, height }) => width && height),
+ distinctUntilChanged(),
+ switchMap(active => active ? annotations$ : EMPTY)
+ )
+ )
+ )
+ }
+ }
+
+ /* Create and return component */
+ return watchCodeBlock(el)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+
+ /* Mount code block lazily */
+ if (feature("content.lazy"))
+ return watchElementVisibility(el)
+ .pipe(
+ filter(visible => visible),
+ take(1),
+ switchMap(() => factory$)
+ )
+
+ /* Mount code block */
+ return factory$
+}
diff --git a/src/templates/assets/javascripts/components/content/code/index.ts b/src/templates/assets/javascripts/components/content/code/index.ts
new file mode 100644
index 00000000..3f86e2b4
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/code/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
diff --git a/src/templates/assets/javascripts/components/content/details/index.ts b/src/templates/assets/javascripts/components/content/details/index.ts
new file mode 100644
index 00000000..17bfae45
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/details/index.ts
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ defer,
+ filter,
+ finalize,
+ map,
+ merge,
+ tap
+} from "rxjs"
+
+import { Component } from "../../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Details
+ */
+export interface Details {
+ action: "open" | "close" /* Details state */
+ reveal?: boolean /* Details is revealed */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ target$: Observable<HTMLElement> /* Location target observable */
+ print$: Observable<boolean> /* Media print observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ target$: Observable<HTMLElement> /* Location target observable */
+ print$: Observable<boolean> /* Media print observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch details
+ *
+ * @param el - Details element
+ * @param options - Options
+ *
+ * @returns Details observable
+ */
+export function watchDetails(
+ el: HTMLDetailsElement, { target$, print$ }: WatchOptions
+): Observable<Details> {
+ let open = true
+ return merge(
+
+ /* Open and focus details on location target */
+ target$
+ .pipe(
+ map(target => target.closest("details:not([open])")!),
+ filter(details => el === details),
+ map(() => ({
+ action: "open", reveal: true
+ }) as Details)
+ ),
+
+ /* Open details on print and close afterwards */
+ print$
+ .pipe(
+ filter(active => active || !open),
+ tap(() => open = el.open),
+ map(active => ({
+ action: active ? "open" : "close"
+ }) as Details)
+ )
+ )
+}
+
+/**
+ * Mount details
+ *
+ * This function ensures that `details` tags are opened on anchor jumps and
+ * prior to printing, so the whole content of the page is visible.
+ *
+ * @param el - Details element
+ * @param options - Options
+ *
+ * @returns Details component observable
+ */
+export function mountDetails(
+ el: HTMLDetailsElement, options: MountOptions
+): Observable<Component<Details>> {
+ return defer(() => {
+ const push$ = new Subject<Details>()
+ push$.subscribe(({ action, reveal }) => {
+ el.toggleAttribute("open", action === "open")
+ if (reveal)
+ el.scrollIntoView()
+ })
+
+ /* Create and return component */
+ return watchDetails(el, options)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/content/index.ts b/src/templates/assets/javascripts/components/content/index.ts
new file mode 100644
index 00000000..a29d8b41
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/index.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./annotation"
+export * from "./code"
+export * from "./details"
+export * from "./table"
+export * from "./tabs"
diff --git a/src/templates/assets/javascripts/components/content/mermaid/index.css b/src/templates/assets/javascripts/components/content/mermaid/index.css
new file mode 100644
index 00000000..3092b8ec
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/mermaid/index.css
@@ -0,0 +1,430 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Rules: general
+ * ------------------------------------------------------------------------- */
+
+/* General node */
+.node circle,
+.node ellipse,
+.node path,
+.node polygon,
+.node rect {
+ fill: var(--md-mermaid-node-bg-color);
+ stroke: var(--md-mermaid-node-fg-color);
+}
+
+/* General marker */
+marker {
+ fill: var(--md-mermaid-edge-color) !important;
+}
+
+/* General edge label */
+.edgeLabel .label rect {
+ fill: transparent;
+}
+
+/* ----------------------------------------------------------------------------
+ * Rules: flowcharts
+ * ------------------------------------------------------------------------- */
+
+/* Flowchart node label */
+.label {
+ color: var(--md-mermaid-label-fg-color);
+ font-family: var(--md-mermaid-font-family);
+}
+
+/* Flowchart node label container */
+.label foreignObject {
+ overflow: visible;
+ line-height: initial;
+}
+
+/* Flowchart edge label in node label */
+.label div .edgeLabel {
+ color: var(--md-mermaid-label-fg-color);
+ background-color: var(--md-mermaid-label-bg-color);
+}
+
+/* Flowchart edge label */
+.edgeLabel,
+.edgeLabel rect {
+ color: var(--md-mermaid-edge-color);
+ background-color: var(--md-mermaid-label-bg-color);
+ fill: var(--md-mermaid-label-bg-color);
+}
+
+/* Flowchart edge path */
+.edgePath .path,
+.flowchart-link {
+ stroke: var(--md-mermaid-edge-color);
+ stroke-width: .05rem;
+}
+
+/* Flowchart arrow head */
+.edgePath .arrowheadPath {
+ fill: var(--md-mermaid-edge-color);
+ stroke: none;
+}
+
+/* Flowchart subgraph */
+.cluster rect {
+ fill: var(--md-default-fg-color--lightest);
+ stroke: var(--md-default-fg-color--lighter);
+}
+
+/* Flowchart subgraph labels */
+.cluster span {
+ color: var(--md-mermaid-label-fg-color);
+ font-family: var(--md-mermaid-font-family);
+}
+
+/* Flowchart markers */
+g #flowchart-circleStart,
+g #flowchart-circleEnd,
+g #flowchart-crossStart,
+g #flowchart-crossEnd,
+g #flowchart-pointStart,
+g #flowchart-pointEnd {
+ stroke: none;
+}
+
+/* ----------------------------------------------------------------------------
+ * Rules: class diagrams
+ * ------------------------------------------------------------------------- */
+
+/* Class group node */
+g.classGroup line,
+g.classGroup rect {
+ fill: var(--md-mermaid-node-bg-color);
+ stroke: var(--md-mermaid-node-fg-color);
+}
+
+/* Class group node text */
+g.classGroup text {
+ font-family: var(--md-mermaid-font-family);
+ fill: var(--md-mermaid-label-fg-color);
+}
+
+/* Class label box */
+.classLabel .box {
+ background-color: var(--md-mermaid-label-bg-color);
+ opacity: 1;
+ fill: var(--md-mermaid-label-bg-color);
+}
+
+/* Class label text */
+.classLabel .label {
+ font-family: var(--md-mermaid-font-family);
+ fill: var(--md-mermaid-label-fg-color);
+}
+
+/* Class group divider */
+.node .divider {
+ stroke: var(--md-mermaid-node-fg-color);
+}
+
+/* Class relation */
+.relation {
+ stroke: var(--md-mermaid-edge-color);
+}
+
+/* Class relation cardinality */
+.cardinality {
+ font-family: var(--md-mermaid-font-family);
+ fill: var(--md-mermaid-label-fg-color);
+}
+
+/* Class relation cardinality text */
+.cardinality text {
+ fill: inherit !important;
+}
+
+/* Class extension, composition and dependency marker */
+defs #classDiagram-extensionStart,
+defs #classDiagram-extensionEnd,
+defs #classDiagram-compositionStart,
+defs #classDiagram-compositionEnd,
+defs #classDiagram-dependencyStart,
+defs #classDiagram-dependencyEnd {
+ fill: var(--md-mermaid-edge-color) !important;
+ stroke: var(--md-mermaid-edge-color) !important;
+}
+
+/* Class aggregation marker */
+defs #classDiagram-aggregationStart,
+defs #classDiagram-aggregationEnd {
+ fill: var(--md-mermaid-label-bg-color) !important;
+ stroke: var(--md-mermaid-edge-color) !important;
+}
+
+/* ----------------------------------------------------------------------------
+ * Rules: state diagrams
+ * ------------------------------------------------------------------------- */
+
+/* State group node */
+g.stateGroup rect {
+ fill: var(--md-mermaid-node-bg-color);
+ stroke: var(--md-mermaid-node-fg-color);
+}
+
+/* State group title */
+g.stateGroup .state-title {
+ font-family: var(--md-mermaid-font-family);
+ fill: var(--md-mermaid-label-fg-color) !important;
+}
+
+/* State group background */
+g.stateGroup .composit {
+ fill: var(--md-mermaid-label-bg-color);
+}
+
+/* State node label */
+.nodeLabel {
+ color: var(--md-mermaid-label-fg-color);
+ font-family: var(--md-mermaid-font-family);
+}
+
+/* State start and end marker */
+.start-state,
+.node circle.state-start,
+.node circle.state-end {
+ fill: var(--md-mermaid-edge-color);
+ stroke: none;
+}
+
+/* State end marker */
+.end-state-outer,
+.end-state-inner {
+ fill: var(--md-mermaid-edge-color);
+}
+
+/* State end marker */
+.end-state-inner,
+.node circle.state-end {
+ stroke: var(--md-mermaid-label-bg-color);
+}
+
+/* State transition */
+.transition {
+ stroke: var(--md-mermaid-edge-color);
+}
+
+/* State fork and join */
+[id^=state-fork] rect,
+[id^=state-join] rect {
+ fill: var(--md-mermaid-edge-color) !important;
+ stroke: none !important;
+}
+
+/* State cluster (yes, 2x... Mermaid WTF) */
+.statediagram-cluster.statediagram-cluster .inner {
+ fill: var(--md-default-bg-color);
+}
+
+/* State cluster node */
+.statediagram-cluster rect {
+ fill: var(--md-mermaid-node-bg-color);
+ stroke: var(--md-mermaid-node-fg-color);
+}
+
+/* State cluster divider */
+.statediagram-state rect.divider {
+ fill: var(--md-default-fg-color--lightest);
+ stroke: var(--md-default-fg-color--lighter);
+}
+
+/* State diagram markers */
+defs #statediagram-barbEnd {
+ stroke: var(--md-mermaid-edge-color);
+}
+
+/* ----------------------------------------------------------------------------
+ * Rules: entity-relationship diagrams
+ * ------------------------------------------------------------------------- */
+
+/* Attribute box */
+.attributeBoxEven,
+.attributeBoxOdd {
+ fill: var(--md-mermaid-node-bg-color);
+ stroke: var(--md-mermaid-node-fg-color);
+}
+
+/* Entity node */
+.entityBox {
+ fill: var(--md-mermaid-label-bg-color);
+ stroke: var(--md-mermaid-node-fg-color);
+}
+
+/* Entity node label */
+.entityLabel {
+ font-family: var(--md-mermaid-font-family);
+ fill: var(--md-mermaid-label-fg-color);
+}
+
+/* Entity relationship label container */
+.relationshipLabelBox {
+ background-color: var(--md-mermaid-label-bg-color);
+ opacity: 1;
+ fill: var(--md-mermaid-label-bg-color);
+ fill-opacity: 1;
+}
+
+/* Entity relationship label */
+.relationshipLabel {
+ fill: var(--md-mermaid-label-fg-color);
+}
+
+/* Entity relationship line { */
+.relationshipLine {
+ stroke: var(--md-mermaid-edge-color);
+}
+
+/* Entity relationship line markers */
+defs #ZERO_OR_ONE_START *,
+defs #ZERO_OR_ONE_END *,
+defs #ZERO_OR_MORE_START *,
+defs #ZERO_OR_MORE_END *,
+defs #ONLY_ONE_START *,
+defs #ONLY_ONE_END *,
+defs #ONE_OR_MORE_START *,
+defs #ONE_OR_MORE_END * {
+ stroke: var(--md-mermaid-edge-color) !important;
+}
+
+/* Entity relationship line markers */
+defs #ZERO_OR_MORE_START circle,
+defs #ZERO_OR_MORE_END circle {
+ fill: var(--md-mermaid-label-bg-color);
+}
+
+/* ----------------------------------------------------------------------------
+ * Rules: sequence diagrams
+ * ------------------------------------------------------------------------- */
+
+/* Sequence actor */
+.actor {
+ fill: var(--md-mermaid-sequence-actor-bg-color);
+ stroke: var(--md-mermaid-sequence-actor-border-color);
+}
+
+/* Sequence actor text */
+text.actor > tspan {
+ font-family: var(--md-mermaid-font-family);
+ fill: var(--md-mermaid-sequence-actor-fg-color);
+}
+
+/* Sequence actor line */
+line {
+ stroke: var(--md-mermaid-sequence-actor-line-color);
+}
+
+/* Sequence actor */
+.actor-man circle,
+.actor-man line {
+ fill: var(--md-mermaid-sequence-actorman-bg-color);
+ stroke: var(--md-mermaid-sequence-actorman-line-color);
+}
+
+/* Sequence message line */
+.messageLine0,
+.messageLine1 {
+ stroke: var(--md-mermaid-sequence-message-line-color);
+}
+
+/* Sequence note */
+.note {
+ fill: var(--md-mermaid-sequence-note-bg-color);
+ stroke: var(--md-mermaid-sequence-note-border-color);
+}
+
+/* Sequence message, loop and note text */
+.messageText,
+.loopText,
+.loopText > tspan,
+.noteText > tspan {
+ font-family: var(--md-mermaid-font-family) !important;
+ stroke: none;
+}
+
+/* Sequence message text */
+.messageText {
+ fill: var(--md-mermaid-sequence-message-fg-color);
+}
+
+/* Sequence loop text */
+.loopText,
+.loopText > tspan {
+ fill: var(--md-mermaid-sequence-loop-fg-color);
+}
+
+/* Sequence note text */
+.noteText > tspan {
+ fill: var(--md-mermaid-sequence-note-fg-color);
+}
+
+/* Sequence arrow head */
+#arrowhead path {
+ fill: var(--md-mermaid-sequence-message-line-color);
+ stroke: none;
+}
+
+/* Sequence loop line */
+.loopLine {
+ fill: var(--md-mermaid-sequence-loop-bg-color);
+ stroke: var(--md-mermaid-sequence-loop-border-color);
+}
+
+/* Sequence label box */
+.labelBox {
+ fill: var(--md-mermaid-sequence-label-bg-color);
+ stroke: none;
+}
+
+/* Sequence label text */
+.labelText,
+.labelText > span {
+ font-family: var(--md-mermaid-font-family);
+ fill: var(--md-mermaid-sequence-label-fg-color);
+}
+
+/* Sequence number */
+.sequenceNumber {
+ fill: var(--md-mermaid-sequence-number-fg-color);
+}
+
+/* Sequence rectangle */
+rect.rect {
+ fill: var(--md-mermaid-sequence-box-bg-color);
+ stroke: none;
+}
+
+/* Sequence rectangle text */
+rect.rect + text.text {
+ fill: var(--md-mermaid-sequence-box-fg-color);
+}
+
+/* Sequence diagram markers */
+defs #sequencenumber {
+ fill: var(--md-mermaid-sequence-number-bg-color) !important;
+}
diff --git a/src/templates/assets/javascripts/components/content/mermaid/index.ts b/src/templates/assets/javascripts/components/content/mermaid/index.ts
new file mode 100644
index 00000000..3f6480fd
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/mermaid/index.ts
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ map,
+ of,
+ shareReplay,
+ tap
+} from "rxjs"
+
+import { watchScript } from "~/browser"
+import { h } from "~/utilities"
+
+import { Component } from "../../_"
+
+import themeCSS from "./index.css"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mermaid diagram
+ */
+export interface Mermaid {}
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mermaid instance observable
+ */
+let mermaid$: Observable<void>
+
+/**
+ * Global sequence number for diagrams
+ */
+let sequence = 0
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Fetch Mermaid script
+ *
+ * @returns Mermaid scripts observable
+ */
+function fetchScripts(): Observable<void> {
+ return typeof mermaid === "undefined" || mermaid instanceof Element
+ ? watchScript("https://unpkg.com/mermaid@9.4.3/dist/mermaid.min.js")
+ : of(undefined)
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount Mermaid diagram
+ *
+ * @param el - Code block element
+ *
+ * @returns Mermaid diagram component observable
+ */
+export function mountMermaid(
+ el: HTMLElement
+): Observable<Component<Mermaid>> {
+ el.classList.remove("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du
+ mermaid$ ||= fetchScripts()
+ .pipe(
+ tap(() => mermaid.initialize({
+ startOnLoad: false,
+ themeCSS,
+ sequence: {
+ actorFontSize: "16px", // Hack: mitigate https://bit.ly/3y0NEi3
+ messageFontSize: "16px",
+ noteFontSize: "16px"
+ }
+ })),
+ map(() => undefined),
+ shareReplay(1)
+ )
+
+ /* Render diagram */
+ mermaid$.subscribe(() => {
+ el.classList.add("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du
+ const id = `__mermaid_${sequence++}`
+
+ /* Create host element to replace code block */
+ const host = h("div", { class: "mermaid" })
+ const text = el.textContent
+
+ /* Render and inject diagram */
+ mermaid.mermaidAPI.render(id, text, (svg: string, fn: Function) => {
+
+ /* Create a shadow root and inject diagram */
+ const shadow = host.attachShadow({ mode: "closed" })
+ shadow.innerHTML = svg
+
+ /* Replace code block with diagram and bind functions */
+ el.replaceWith(host)
+ fn?.(shadow)
+ })
+ })
+
+ /* Create and return component */
+ return mermaid$
+ .pipe(
+ map(() => ({ ref: el }))
+ )
+}
diff --git a/src/templates/assets/javascripts/components/content/table/index.ts b/src/templates/assets/javascripts/components/content/table/index.ts
new file mode 100644
index 00000000..c318e7a6
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/table/index.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { Observable, of } from "rxjs"
+
+import { renderTable } from "~/templates"
+import { h } from "~/utilities"
+
+import { Component } from "../../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Data table
+ */
+export interface DataTable {}
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Sentinel for replacement
+ */
+const sentinel = h("table")
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount data table
+ *
+ * This function wraps a data table in another scrollable container, so it can
+ * be smoothly scrolled on smaller screen sizes and won't break the layout.
+ *
+ * @param el - Data table element
+ *
+ * @returns Data table component observable
+ */
+export function mountDataTable(
+ el: HTMLElement
+): Observable<Component<DataTable>> {
+ el.replaceWith(sentinel)
+ sentinel.replaceWith(renderTable(el))
+
+ /* Create and return component */
+ return of({ ref: el })
+}
diff --git a/src/templates/assets/javascripts/components/content/tabs/index.ts b/src/templates/assets/javascripts/components/content/tabs/index.ts
new file mode 100644
index 00000000..f57447e2
--- /dev/null
+++ b/src/templates/assets/javascripts/components/content/tabs/index.ts
@@ -0,0 +1,265 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ animationFrameScheduler,
+ asyncScheduler,
+ auditTime,
+ combineLatest,
+ defer,
+ endWith,
+ finalize,
+ fromEvent,
+ ignoreElements,
+ map,
+ merge,
+ skip,
+ startWith,
+ subscribeOn,
+ takeUntil,
+ tap,
+ withLatestFrom
+} from "rxjs"
+
+import { feature } from "~/_"
+import {
+ Viewport,
+ getElement,
+ getElementContentOffset,
+ getElementContentSize,
+ getElementOffset,
+ getElementSize,
+ getElements,
+ watchElementContentOffset,
+ watchElementSize
+} from "~/browser"
+import { renderTabbedControl } from "~/templates"
+
+import { Component } from "../../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Content tabs
+ */
+export interface ContentTabs {
+ active: HTMLLabelElement /* Active tab label */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch content tabs
+ *
+ * @param el - Content tabs element
+ *
+ * @returns Content tabs observable
+ */
+export function watchContentTabs(
+ el: HTMLElement
+): Observable<ContentTabs> {
+ const inputs = getElements<HTMLInputElement>(":scope > input", el)
+ const initial = inputs.find(input => input.checked) || inputs[0]
+ return merge(...inputs.map(input => fromEvent(input, "change")
+ .pipe(
+ map(() => getElement<HTMLLabelElement>(`label[for="${input.id}"]`))
+ )
+ ))
+ .pipe(
+ startWith(getElement<HTMLLabelElement>(`label[for="${initial.id}"]`)),
+ map(active => ({ active }))
+ )
+}
+
+/**
+ * Mount content tabs
+ *
+ * This function scrolls the active tab into view. While this functionality is
+ * provided by browsers as part of `scrollInfoView`, browsers will always also
+ * scroll the vertical axis, which we do not want. Thus, we decided to provide
+ * this functionality ourselves.
+ *
+ * @param el - Content tabs element
+ * @param options - Options
+ *
+ * @returns Content tabs component observable
+ */
+export function mountContentTabs(
+ el: HTMLElement, { viewport$ }: MountOptions
+): Observable<Component<ContentTabs>> {
+
+ /* Render content tab previous button for pagination */
+ const prev = renderTabbedControl("prev")
+ el.append(prev)
+
+ /* Render content tab next button for pagination */
+ const next = renderTabbedControl("next")
+ el.append(next)
+
+ /* Mount component on subscription */
+ const container = getElement(".tabbed-labels", el)
+ return defer(() => {
+ const push$ = new Subject<ContentTabs>()
+ const done$ = push$.pipe(ignoreElements(), endWith(true))
+ combineLatest([push$, watchElementSize(el)])
+ .pipe(
+ auditTime(1, animationFrameScheduler),
+ takeUntil(done$)
+ )
+ .subscribe({
+
+ /* Handle emission */
+ next([{ active }, size]) {
+ const offset = getElementOffset(active)
+ const { width } = getElementSize(active)
+
+ /* Set tab indicator offset and width */
+ el.style.setProperty("--md-indicator-x", `${offset.x}px`)
+ el.style.setProperty("--md-indicator-width", `${width}px`)
+
+ /* Scroll container to active content tab */
+ const content = getElementContentOffset(container)
+ if (
+ offset.x < content.x ||
+ offset.x + width > content.x + size.width
+ )
+ container.scrollTo({
+ left: Math.max(0, offset.x - 16),
+ behavior: "smooth"
+ })
+ },
+
+ /* Handle complete */
+ complete() {
+ el.style.removeProperty("--md-indicator-x")
+ el.style.removeProperty("--md-indicator-width")
+ }
+ })
+
+ /* Hide content tab buttons on borders */
+ combineLatest([
+ watchElementContentOffset(container),
+ watchElementSize(container)
+ ])
+ .pipe(
+ takeUntil(done$)
+ )
+ .subscribe(([offset, size]) => {
+ const content = getElementContentSize(container)
+ prev.hidden = offset.x < 16
+ next.hidden = offset.x > content.width - size.width - 16
+ })
+
+ /* Paginate content tab container on click */
+ merge(
+ fromEvent(prev, "click").pipe(map(() => -1)),
+ fromEvent(next, "click").pipe(map(() => +1))
+ )
+ .pipe(
+ takeUntil(done$)
+ )
+ .subscribe(direction => {
+ const { width } = getElementSize(container)
+ container.scrollBy({
+ left: width * direction,
+ behavior: "smooth"
+ })
+ })
+
+ /* Set up linking of content tabs, if enabled */
+ if (feature("content.tabs.link"))
+ push$.pipe(
+ skip(1),
+ withLatestFrom(viewport$)
+ )
+ .subscribe(([{ active }, { offset }]) => {
+ const tab = active.innerText.trim()
+ if (active.hasAttribute("data-md-switching")) {
+ active.removeAttribute("data-md-switching")
+
+ /* Determine viewport offset of active tab */
+ } else {
+ const y = el.offsetTop - offset.y
+
+ /* Passively activate other tabs */
+ for (const set of getElements("[data-tabs]"))
+ for (const input of getElements<HTMLInputElement>(
+ ":scope > input", set
+ )) {
+ const label = getElement(`label[for="${input.id}"]`)
+ if (
+ label !== active &&
+ label.innerText.trim() === tab
+ ) {
+ label.setAttribute("data-md-switching", "")
+ input.click()
+ break
+ }
+ }
+
+ /* Bring active tab into view */
+ window.scrollTo({
+ top: el.offsetTop - y
+ })
+
+ /* Persist active tabs in local storage */
+ const tabs = __md_get<string[]>("__tabs") || []
+ __md_set("__tabs", [...new Set([tab, ...tabs])])
+ }
+ })
+
+ /* Pause media (audio, video) on switch - see https://bit.ly/3Bk6cel */
+ push$.pipe(takeUntil(done$))
+ .subscribe(() => {
+ for (const media of getElements<HTMLAudioElement>("audio, video", el))
+ media.pause()
+ })
+
+ /* Create and return component */
+ return watchContentTabs(el)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+ .pipe(
+ subscribeOn(asyncScheduler)
+ )
+}
diff --git a/src/templates/assets/javascripts/components/dialog/index.ts b/src/templates/assets/javascripts/components/dialog/index.ts
new file mode 100644
index 00000000..6ff1bd44
--- /dev/null
+++ b/src/templates/assets/javascripts/components/dialog/index.ts
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ defer,
+ delay,
+ finalize,
+ map,
+ merge,
+ of,
+ switchMap,
+ tap
+} from "rxjs"
+
+import { getElement } from "~/browser"
+
+import { Component } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Dialog
+ */
+export interface Dialog {
+ message: string /* Dialog message */
+ active: boolean /* Dialog is active */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ alert$: Subject<string> /* Alert subject */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ alert$: Subject<string> /* Alert subject */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch dialog
+ *
+ * @param _el - Dialog element
+ * @param options - Options
+ *
+ * @returns Dialog observable
+ */
+export function watchDialog(
+ _el: HTMLElement, { alert$ }: WatchOptions
+): Observable<Dialog> {
+ return alert$
+ .pipe(
+ switchMap(message => merge(
+ of(true),
+ of(false).pipe(delay(2000))
+ )
+ .pipe(
+ map(active => ({ message, active }))
+ )
+ )
+ )
+}
+
+/**
+ * Mount dialog
+ *
+ * This function reveals the dialog in the right corner when a new alert is
+ * emitted through the subject that is passed as part of the options.
+ *
+ * @param el - Dialog element
+ * @param options - Options
+ *
+ * @returns Dialog component observable
+ */
+export function mountDialog(
+ el: HTMLElement, options: MountOptions
+): Observable<Component<Dialog>> {
+ const inner = getElement(".md-typeset", el)
+ return defer(() => {
+ const push$ = new Subject<Dialog>()
+ push$.subscribe(({ message, active }) => {
+ el.classList.toggle("md-dialog--active", active)
+ inner.textContent = message
+ })
+
+ /* Create and return component */
+ return watchDialog(el, options)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/header/_/index.ts b/src/templates/assets/javascripts/components/header/_/index.ts
new file mode 100644
index 00000000..0f33eb48
--- /dev/null
+++ b/src/templates/assets/javascripts/components/header/_/index.ts
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ bufferCount,
+ combineLatest,
+ combineLatestWith,
+ defer,
+ distinctUntilChanged,
+ distinctUntilKeyChanged,
+ endWith,
+ filter,
+ ignoreElements,
+ map,
+ of,
+ shareReplay,
+ startWith,
+ switchMap,
+ takeUntil
+} from "rxjs"
+
+import { feature } from "~/_"
+import {
+ Viewport,
+ watchElementSize,
+ watchToggle
+} from "~/browser"
+
+import { Component } from "../../_"
+import { Main } from "../../main"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Header
+ */
+export interface Header {
+ height: number /* Header visible height */
+ hidden: boolean /* Header is hidden */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+ main$: Observable<Main> /* Main area observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Compute whether the header is hidden
+ *
+ * If the user scrolls past a certain threshold, the header can be hidden when
+ * scrolling down, and shown when scrolling up.
+ *
+ * @param options - Options
+ *
+ * @returns Toggle observable
+ */
+function isHidden({ viewport$ }: WatchOptions): Observable<boolean> {
+ if (!feature("header.autohide"))
+ return of(false)
+
+ /* Compute direction and turning point */
+ const direction$ = viewport$
+ .pipe(
+ map(({ offset: { y } }) => y),
+ bufferCount(2, 1),
+ map(([a, b]) => [a < b, b] as const),
+ distinctUntilKeyChanged(0)
+ )
+
+ /* Compute whether header should be hidden */
+ const hidden$ = combineLatest([viewport$, direction$])
+ .pipe(
+ filter(([{ offset }, [, y]]) => Math.abs(y - offset.y) > 100),
+ map(([, [direction]]) => direction),
+ distinctUntilChanged()
+ )
+
+ /* Compute threshold for hiding */
+ const search$ = watchToggle("search")
+ return combineLatest([viewport$, search$])
+ .pipe(
+ map(([{ offset }, search]) => offset.y > 400 && !search),
+ distinctUntilChanged(),
+ switchMap(active => active ? hidden$ : of(false)),
+ startWith(false)
+ )
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch header
+ *
+ * @param el - Header element
+ * @param options - Options
+ *
+ * @returns Header observable
+ */
+export function watchHeader(
+ el: HTMLElement, options: WatchOptions
+): Observable<Header> {
+ return defer(() => combineLatest([
+ watchElementSize(el),
+ isHidden(options)
+ ]))
+ .pipe(
+ map(([{ height }, hidden]) => ({
+ height,
+ hidden
+ })),
+ distinctUntilChanged((a, b) => (
+ a.height === b.height &&
+ a.hidden === b.hidden
+ )),
+ shareReplay(1)
+ )
+}
+
+/**
+ * Mount header
+ *
+ * This function manages the different states of the header, i.e. whether it's
+ * hidden or rendered with a shadow. This depends heavily on the main area.
+ *
+ * @param el - Header element
+ * @param options - Options
+ *
+ * @returns Header component observable
+ */
+export function mountHeader(
+ el: HTMLElement, { header$, main$ }: MountOptions
+): Observable<Component<Header>> {
+ return defer(() => {
+ const push$ = new Subject<Main>()
+ const done$ = push$.pipe(ignoreElements(), endWith(true))
+ push$
+ .pipe(
+ distinctUntilKeyChanged("active"),
+ combineLatestWith(header$)
+ )
+ .subscribe(([{ active }, { hidden }]) => {
+ el.classList.toggle("md-header--shadow", active && !hidden)
+ el.hidden = hidden
+ })
+
+ /* Link to main area */
+ main$.subscribe(push$)
+
+ /* Create and return component */
+ return header$
+ .pipe(
+ takeUntil(done$),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/header/index.ts b/src/templates/assets/javascripts/components/header/index.ts
new file mode 100644
index 00000000..cf23ec1a
--- /dev/null
+++ b/src/templates/assets/javascripts/components/header/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./title"
diff --git a/src/templates/assets/javascripts/components/header/title/index.ts b/src/templates/assets/javascripts/components/header/title/index.ts
new file mode 100644
index 00000000..f3bc0d08
--- /dev/null
+++ b/src/templates/assets/javascripts/components/header/title/index.ts
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Observable,
+ Subject,
+ defer,
+ distinctUntilKeyChanged,
+ finalize,
+ map,
+ tap
+} from "rxjs"
+
+import {
+ Viewport,
+ getElementSize,
+ getOptionalElement,
+ watchViewportAt
+} from "~/browser"
+
+import { Component } from "../../_"
+import { Header } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Header
+ */
+export interface HeaderTitle {
+ active: boolean /* Header title is active */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch header title
+ *
+ * @param el - Heading element
+ * @param options - Options
+ *
+ * @returns Header title observable
+ */
+export function watchHeaderTitle(
+ el: HTMLElement, { viewport$, header$ }: WatchOptions
+): Observable<HeaderTitle> {
+ return watchViewportAt(el, { viewport$, header$ })
+ .pipe(
+ map(({ offset: { y } }) => {
+ const { height } = getElementSize(el)
+ return {
+ active: y >= height
+ }
+ }),
+ distinctUntilKeyChanged("active")
+ )
+}
+
+/**
+ * Mount header title
+ *
+ * This function swaps the header title from the site title to the title of the
+ * current page when the user scrolls past the first headline.
+ *
+ * @param el - Header title element
+ * @param options - Options
+ *
+ * @returns Header title component observable
+ */
+export function mountHeaderTitle(
+ el: HTMLElement, options: MountOptions
+): Observable<Component<HeaderTitle>> {
+ return defer(() => {
+ const push$ = new Subject<HeaderTitle>()
+ push$.subscribe({
+
+ /* Handle emission */
+ next({ active }) {
+ el.classList.toggle("md-header__title--active", active)
+ },
+
+ /* Handle complete */
+ complete() {
+ el.classList.remove("md-header__title--active")
+ }
+ })
+
+ /* Obtain headline, if any */
+ const heading = getOptionalElement(".md-content h1")
+ if (typeof heading === "undefined")
+ return EMPTY
+
+ /* Create and return component */
+ return watchHeaderTitle(heading, options)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/index.ts b/src/templates/assets/javascripts/components/index.ts
new file mode 100644
index 00000000..3d4391d1
--- /dev/null
+++ b/src/templates/assets/javascripts/components/index.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./announce"
+export * from "./consent"
+export * from "./content"
+export * from "./dialog"
+export * from "./header"
+export * from "./main"
+export * from "./palette"
+export * from "./progress"
+export * from "./search"
+export * from "./sidebar"
+export * from "./source"
+export * from "./tabs"
+export * from "./toc"
+export * from "./top"
diff --git a/src/templates/assets/javascripts/components/main/index.ts b/src/templates/assets/javascripts/components/main/index.ts
new file mode 100644
index 00000000..2509f9b9
--- /dev/null
+++ b/src/templates/assets/javascripts/components/main/index.ts
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ combineLatest,
+ distinctUntilChanged,
+ distinctUntilKeyChanged,
+ map,
+ switchMap
+} from "rxjs"
+
+import {
+ Viewport,
+ watchElementSize
+} from "~/browser"
+
+import { Header } from "../header"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Main area
+ */
+export interface Main {
+ offset: number /* Main area top offset */
+ height: number /* Main area visible height */
+ active: boolean /* Main area is active */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch main area
+ *
+ * This function returns an observable that computes the visual parameters of
+ * the main area which depends on the viewport vertical offset and height, as
+ * well as the height of the header element, if the header is fixed.
+ *
+ * @param el - Main area element
+ * @param options - Options
+ *
+ * @returns Main area observable
+ */
+export function watchMain(
+ el: HTMLElement, { viewport$, header$ }: WatchOptions
+): Observable<Main> {
+
+ /* Compute necessary adjustment for header */
+ const adjust$ = header$
+ .pipe(
+ map(({ height }) => height),
+ distinctUntilChanged()
+ )
+
+ /* Compute the main area's top and bottom borders */
+ const border$ = adjust$
+ .pipe(
+ switchMap(() => watchElementSize(el)
+ .pipe(
+ map(({ height }) => ({
+ top: el.offsetTop,
+ bottom: el.offsetTop + height
+ })),
+ distinctUntilKeyChanged("bottom")
+ )
+ )
+ )
+
+ /* Compute the main area's offset, visible height and if we scrolled past */
+ return combineLatest([adjust$, border$, viewport$])
+ .pipe(
+ map(([header, { top, bottom }, { offset: { y }, size: { height } }]) => {
+ height = Math.max(0, height
+ - Math.max(0, top - y, header)
+ - Math.max(0, height + y - bottom)
+ )
+ return {
+ offset: top - header,
+ height,
+ active: top - header <= y
+ }
+ }),
+ distinctUntilChanged((a, b) => (
+ a.offset === b.offset &&
+ a.height === b.height &&
+ a.active === b.active
+ ))
+ )
+}
diff --git a/src/templates/assets/javascripts/components/palette/index.ts b/src/templates/assets/javascripts/components/palette/index.ts
new file mode 100644
index 00000000..cf578f60
--- /dev/null
+++ b/src/templates/assets/javascripts/components/palette/index.ts
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ asyncScheduler,
+ defer,
+ finalize,
+ fromEvent,
+ map,
+ mergeMap,
+ observeOn,
+ of,
+ shareReplay,
+ startWith,
+ tap
+} from "rxjs"
+
+import { getElements } from "~/browser"
+import { h } from "~/utilities"
+
+import {
+ Component,
+ getComponentElement
+} from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Palette colors
+ */
+export interface PaletteColor {
+ scheme?: string /* Color scheme */
+ primary?: string /* Primary color */
+ accent?: string /* Accent color */
+}
+
+/**
+ * Palette
+ */
+export interface Palette {
+ index: number /* Palette index */
+ color: PaletteColor /* Palette colors */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch color palette
+ *
+ * @param inputs - Color palette element
+ *
+ * @returns Color palette observable
+ */
+export function watchPalette(
+ inputs: HTMLInputElement[]
+): Observable<Palette> {
+ const current = __md_get<Palette>("__palette") || {
+ index: inputs.findIndex(input => matchMedia(
+ input.getAttribute("data-md-color-media")!
+ ).matches)
+ }
+
+ /* Emit changes in color palette */
+ return of(...inputs)
+ .pipe(
+ mergeMap(input => fromEvent(input, "change")
+ .pipe(
+ map(() => input)
+ )
+ ),
+ startWith(inputs[Math.max(0, current.index)]),
+ map(input => ({
+ index: inputs.indexOf(input),
+ color: {
+ scheme: input.getAttribute("data-md-color-scheme"),
+ primary: input.getAttribute("data-md-color-primary"),
+ accent: input.getAttribute("data-md-color-accent")
+ }
+ } as Palette)),
+ shareReplay(1)
+ )
+}
+
+/**
+ * Mount color palette
+ *
+ * @param el - Color palette element
+ *
+ * @returns Color palette component observable
+ */
+export function mountPalette(
+ el: HTMLElement
+): Observable<Component<Palette>> {
+ const meta = h("meta", { name: "theme-color" })
+ document.head.appendChild(meta)
+
+ // Add color scheme meta tag
+ const scheme = h("meta", { name: "color-scheme" })
+ document.head.appendChild(scheme)
+
+ /* Mount component on subscription */
+ return defer(() => {
+ const push$ = new Subject<Palette>()
+ push$.subscribe(palette => {
+ document.body.setAttribute("data-md-color-switching", "")
+
+ /* Set color palette */
+ for (const [key, value] of Object.entries(palette.color))
+ document.body.setAttribute(`data-md-color-${key}`, value)
+
+ /* Toggle visibility */
+ for (let index = 0; index < inputs.length; index++) {
+ const label = inputs[index].nextElementSibling
+ if (label instanceof HTMLElement)
+ label.hidden = palette.index !== index
+ }
+
+ /* Persist preference in local storage */
+ __md_set("__palette", palette)
+ })
+
+ /* Update theme-color meta tag */
+ push$
+ .pipe(
+ map(() => {
+ const header = getComponentElement("header")
+ const style = window.getComputedStyle(header)
+
+ // Set color scheme
+ scheme.content = style.colorScheme
+
+ /* Return color in hexadecimal format */
+ return style.backgroundColor.match(/\d+/g)!
+ .map(value => (+value).toString(16).padStart(2, "0"))
+ .join("")
+ })
+ )
+ .subscribe(color => meta.content = `#${color}`)
+
+ /* Revert transition durations after color switch */
+ push$.pipe(observeOn(asyncScheduler))
+ .subscribe(() => {
+ document.body.removeAttribute("data-md-color-switching")
+ })
+
+ /* Create and return component */
+ const inputs = getElements<HTMLInputElement>("input", el)
+ return watchPalette(inputs)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/progress/index.ts b/src/templates/assets/javascripts/components/progress/index.ts
new file mode 100644
index 00000000..30c722b8
--- /dev/null
+++ b/src/templates/assets/javascripts/components/progress/index.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ defer,
+ finalize,
+ map,
+ tap
+} from "rxjs"
+
+import { Component } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Progress indicator
+ */
+export interface Progress {
+ value: number // Progress value
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ progress$: Subject<number> // Progress subject
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount progress indicator
+ *
+ * @param el - Progress indicator element
+ * @param options - Options
+ *
+ * @returns Progress indicator component observable
+ */
+export function mountProgress(
+ el: HTMLElement, { progress$ }: MountOptions
+): Observable<Component<Progress>> {
+
+ // Mount component on subscription
+ return defer(() => {
+ const push$ = new Subject<Progress>()
+ push$.subscribe(({ value }) => {
+ el.style.setProperty("--md-progress-value", `${value}`)
+ })
+
+ // Create and return component
+ return progress$
+ .pipe(
+ tap(value => push$.next({ value })),
+ finalize(() => push$.complete()),
+ map(value => ({ ref: el, value }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/search/_/index.ts b/src/templates/assets/javascripts/components/search/_/index.ts
new file mode 100644
index 00000000..aa963b47
--- /dev/null
+++ b/src/templates/assets/javascripts/components/search/_/index.ts
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ NEVER,
+ Observable,
+ ObservableInput,
+ filter,
+ fromEvent,
+ merge,
+ mergeWith
+} from "rxjs"
+
+import { configuration } from "~/_"
+import {
+ Keyboard,
+ getActiveElement,
+ getElements,
+ setToggle
+} from "~/browser"
+import {
+ SearchIndex,
+ SearchResult,
+ setupSearchWorker
+} from "~/integrations"
+
+import {
+ Component,
+ getComponentElement,
+ getComponentElements
+} from "../../_"
+import {
+ SearchQuery,
+ mountSearchQuery
+} from "../query"
+import { mountSearchResult } from "../result"
+import {
+ SearchShare,
+ mountSearchShare
+} from "../share"
+import {
+ SearchSuggest,
+ mountSearchSuggest
+} from "../suggest"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search
+ */
+export type Search =
+ | SearchQuery
+ | SearchResult
+ | SearchShare
+ | SearchSuggest
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ index$: ObservableInput<SearchIndex> /* Search index observable */
+ keyboard$: Observable<Keyboard> /* Keyboard observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount search
+ *
+ * This function sets up the search functionality, including the underlying
+ * web worker and all keyboard bindings.
+ *
+ * @param el - Search element
+ * @param options - Options
+ *
+ * @returns Search component observable
+ */
+export function mountSearch(
+ el: HTMLElement, { index$, keyboard$ }: MountOptions
+): Observable<Component<Search>> {
+ const config = configuration()
+ try {
+ const worker$ = setupSearchWorker(config.search, index$)
+
+ /* Retrieve query and result components */
+ const query = getComponentElement("search-query", el)
+ const result = getComponentElement("search-result", el)
+
+ /* Always close search on result selection */
+ fromEvent<PointerEvent>(el, "click")
+ .pipe(
+ filter(({ target }) => (
+ target instanceof Element && !!target.closest("a")
+ ))
+ )
+ .subscribe(() => setToggle("search", false))
+
+ /* Set up search keyboard handlers */
+ keyboard$
+ .pipe(
+ filter(({ mode }) => mode === "search")
+ )
+ .subscribe(key => {
+ const active = getActiveElement()
+ switch (key.type) {
+
+ /* Enter: go to first (best) result */
+ case "Enter":
+ if (active === query) {
+ const anchors = new Map<HTMLAnchorElement, number>()
+ for (const anchor of getElements<HTMLAnchorElement>(
+ ":first-child [href]", result
+ )) {
+ const article = anchor.firstElementChild!
+ anchors.set(anchor, parseFloat(
+ article.getAttribute("data-md-score")!
+ ))
+ }
+
+ /* Go to result with highest score, if any */
+ if (anchors.size) {
+ const [[best]] = [...anchors].sort(([, a], [, b]) => b - a)
+ best.click()
+ }
+
+ /* Otherwise omit form submission */
+ key.claim()
+ }
+ break
+
+ /* Escape or Tab: close search */
+ case "Escape":
+ case "Tab":
+ setToggle("search", false)
+ query.blur()
+ break
+
+ /* Vertical arrows: select previous or next search result */
+ case "ArrowUp":
+ case "ArrowDown":
+ if (typeof active === "undefined") {
+ query.focus()
+ } else {
+ const els = [query, ...getElements(
+ ":not(details) > [href], summary, details[open] [href]",
+ result
+ )]
+ const i = Math.max(0, (
+ Math.max(0, els.indexOf(active)) + els.length + (
+ key.type === "ArrowUp" ? -1 : +1
+ )
+ ) % els.length)
+ els[i].focus()
+ }
+
+ /* Prevent scrolling of page */
+ key.claim()
+ break
+
+ /* All other keys: hand to search query */
+ default:
+ if (query !== getActiveElement())
+ query.focus()
+ }
+ })
+
+ /* Set up global keyboard handlers */
+ keyboard$
+ .pipe(
+ filter(({ mode }) => mode === "global")
+ )
+ .subscribe(key => {
+ switch (key.type) {
+
+ /* Open search and select query */
+ case "f":
+ case "s":
+ case "/":
+ query.focus()
+ query.select()
+
+ /* Prevent scrolling of page */
+ key.claim()
+ break
+ }
+ })
+
+ /* Create and return component */
+ const query$ = mountSearchQuery(query, { worker$ })
+ return merge(
+ query$,
+ mountSearchResult(result, { worker$, query$ })
+ )
+ .pipe(
+ mergeWith(
+
+ /* Search sharing */
+ ...getComponentElements("search-share", el)
+ .map(child => mountSearchShare(child, { query$ })),
+
+ /* Search suggestions */
+ ...getComponentElements("search-suggest", el)
+ .map(child => mountSearchSuggest(child, { worker$, keyboard$ }))
+ )
+ )
+
+ /* Gracefully handle broken search */
+ } catch (err) {
+ el.hidden = true
+ return NEVER
+ }
+}
diff --git a/src/templates/assets/javascripts/components/search/highlight/.eslintrc b/src/templates/assets/javascripts/components/search/highlight/.eslintrc
new file mode 100644
index 00000000..38a5714d
--- /dev/null
+++ b/src/templates/assets/javascripts/components/search/highlight/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "rules": {
+ "no-null/no-null": "off"
+ }
+}
diff --git a/src/templates/assets/javascripts/components/search/highlight/index.ts b/src/templates/assets/javascripts/components/search/highlight/index.ts
new file mode 100644
index 00000000..bc3f94c9
--- /dev/null
+++ b/src/templates/assets/javascripts/components/search/highlight/index.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ ObservableInput,
+ combineLatest,
+ filter,
+ map,
+ startWith
+} from "rxjs"
+
+import { getLocation } from "~/browser"
+import {
+ SearchIndex,
+ setupSearchHighlighter
+} from "~/integrations"
+import { h } from "~/utilities"
+
+import { Component } from "../../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search highlighting
+ */
+export interface SearchHighlight {
+ nodes: Map<ChildNode, string> /* Map of replacements */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ index$: ObservableInput<SearchIndex> /* Search index observable */
+ location$: Observable<URL> /* Location observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount search highlighting
+ *
+ * @param el - Content element
+ * @param options - Options
+ *
+ * @returns Search highlighting component observable
+ */
+export function mountSearchHiglight(
+ el: HTMLElement, { index$, location$ }: MountOptions
+): Observable<Component<SearchHighlight>> {
+ return combineLatest([
+ index$,
+ location$
+ .pipe(
+ startWith(getLocation()),
+ filter(url => !!url.searchParams.get("h"))
+ )
+ ])
+ .pipe(
+ map(([index, url]) => setupSearchHighlighter(index.config)(
+ url.searchParams.get("h")!
+ )),
+ map(fn => {
+ const nodes = new Map<ChildNode, string>()
+
+ /* Traverse text nodes and collect matches */
+ const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT)
+ for (let node = it.nextNode(); node; node = it.nextNode()) {
+ if (node.parentElement?.offsetHeight) {
+ const original = node.textContent!
+ const replaced = fn(original)
+ if (replaced.length > original.length)
+ nodes.set(node as ChildNode, replaced)
+ }
+ }
+
+ /* Replace original nodes with matches */
+ for (const [node, text] of nodes) {
+ const { childNodes } = h("span", null, text)
+ node.replaceWith(...Array.from(childNodes))
+ }
+
+ /* Return component */
+ return { ref: el, nodes }
+ })
+ )
+}
diff --git a/src/templates/assets/javascripts/components/search/index.ts b/src/templates/assets/javascripts/components/search/index.ts
new file mode 100644
index 00000000..846d8685
--- /dev/null
+++ b/src/templates/assets/javascripts/components/search/index.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./highlight"
+export * from "./query"
+export * from "./result"
+export * from "./share"
+export * from "./suggest"
diff --git a/src/templates/assets/javascripts/components/search/query/index.ts b/src/templates/assets/javascripts/components/search/query/index.ts
new file mode 100644
index 00000000..4ce21279
--- /dev/null
+++ b/src/templates/assets/javascripts/components/search/query/index.ts
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ combineLatest,
+ distinctUntilChanged,
+ distinctUntilKeyChanged,
+ endWith,
+ finalize,
+ first,
+ fromEvent,
+ ignoreElements,
+ map,
+ merge,
+ shareReplay,
+ takeUntil,
+ tap
+} from "rxjs"
+
+import {
+ getElement,
+ getLocation,
+ setToggle,
+ watchElementFocus,
+ watchToggle
+} from "~/browser"
+import {
+ SearchMessage,
+ SearchMessageType,
+ isSearchReadyMessage
+} from "~/integrations"
+
+import { Component } from "../../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search query
+ */
+export interface SearchQuery {
+ value: string /* Query value */
+ focus: boolean /* Query focus */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ worker$: Subject<SearchMessage> /* Search worker */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ worker$: Subject<SearchMessage> /* Search worker */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch search query
+ *
+ * Note that the focus event which triggers re-reading the current query value
+ * is delayed by `1ms` so the input's empty state is allowed to propagate.
+ *
+ * @param el - Search query element
+ * @param options - Options
+ *
+ * @returns Search query observable
+ */
+export function watchSearchQuery(
+ el: HTMLInputElement, { worker$ }: WatchOptions
+): Observable<SearchQuery> {
+
+ /* Support search deep linking */
+ const { searchParams } = getLocation()
+ if (searchParams.has("q")) {
+ setToggle("search", true)
+
+ /* Set query from parameter */
+ el.value = searchParams.get("q")!
+ el.focus()
+
+ /* Remove query parameter on close */
+ watchToggle("search")
+ .pipe(
+ first(active => !active)
+ )
+ .subscribe(() => {
+ const url = getLocation()
+ url.searchParams.delete("q")
+ history.replaceState({}, "", `${url}`)
+ })
+ }
+
+ /* Intercept focus and input events */
+ const focus$ = watchElementFocus(el)
+ const value$ = merge(
+ worker$.pipe(first(isSearchReadyMessage)),
+ fromEvent(el, "keyup"),
+ focus$
+ )
+ .pipe(
+ map(() => el.value),
+ distinctUntilChanged()
+ )
+
+ /* Combine into single observable */
+ return combineLatest([value$, focus$])
+ .pipe(
+ map(([value, focus]) => ({ value, focus })),
+ shareReplay(1)
+ )
+}
+
+/**
+ * Mount search query
+ *
+ * @param el - Search query element
+ * @param options - Options
+ *
+ * @returns Search query component observable
+ */
+export function mountSearchQuery(
+ el: HTMLInputElement, { worker$ }: MountOptions
+): Observable<Component<SearchQuery, HTMLInputElement>> {
+ const push$ = new Subject<SearchQuery>()
+ const done$ = push$.pipe(ignoreElements(), endWith(true))
+
+ /* Handle value change */
+ combineLatest([
+ worker$.pipe(first(isSearchReadyMessage)),
+ push$
+ ], (_, query) => query)
+ .pipe(
+ distinctUntilKeyChanged("value")
+ )
+ .subscribe(({ value }) => worker$.next({
+ type: SearchMessageType.QUERY,
+ data: value
+ }))
+
+ /* Handle focus change */
+ push$
+ .pipe(
+ distinctUntilKeyChanged("focus")
+ )
+ .subscribe(({ focus }) => {
+ if (focus)
+ setToggle("search", focus)
+ })
+
+ /* Handle reset */
+ fromEvent(el.form!, "reset")
+ .pipe(
+ takeUntil(done$)
+ )
+ .subscribe(() => el.focus())
+
+ // Focus search query on label click - note that this is necessary to bring
+ // up the keyboard on iOS and other mobile platforms, as the search dialog is
+ // not visible at first, and programatically focusing an input element must
+ // be triggered by a user interaction - see https://t.ly/Cb30n
+ const label = getElement("header [for=__search]")
+ fromEvent(label, "click")
+ .subscribe(() => el.focus())
+
+ /* Create and return component */
+ return watchSearchQuery(el, { worker$ })
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state })),
+ shareReplay(1)
+ )
+}
diff --git a/src/templates/assets/javascripts/components/search/result/index.ts b/src/templates/assets/javascripts/components/search/result/index.ts
new file mode 100644
index 00000000..c3c9ef20
--- /dev/null
+++ b/src/templates/assets/javascripts/components/search/result/index.ts
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Observable,
+ Subject,
+ bufferCount,
+ filter,
+ finalize,
+ first,
+ fromEvent,
+ map,
+ merge,
+ mergeMap,
+ of,
+ share,
+ skipUntil,
+ switchMap,
+ takeUntil,
+ tap,
+ withLatestFrom,
+ zipWith
+} from "rxjs"
+
+import { translation } from "~/_"
+import {
+ getElement,
+ getOptionalElement,
+ watchElementBoundary,
+ watchToggle
+} from "~/browser"
+import {
+ SearchMessage,
+ SearchResult,
+ isSearchReadyMessage,
+ isSearchResultMessage
+} from "~/integrations"
+import { renderSearchResultItem } from "~/templates"
+import { round } from "~/utilities"
+
+import { Component } from "../../_"
+import { SearchQuery } from "../query"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ query$: Observable<SearchQuery> /* Search query observable */
+ worker$: Subject<SearchMessage> /* Search worker */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount search result list
+ *
+ * This function performs a lazy rendering of the search results, depending on
+ * the vertical offset of the search result container.
+ *
+ * @param el - Search result list element
+ * @param options - Options
+ *
+ * @returns Search result list component observable
+ */
+export function mountSearchResult(
+ el: HTMLElement, { worker$, query$ }: MountOptions
+): Observable<Component<SearchResult>> {
+ const push$ = new Subject<SearchResult>()
+ const boundary$ = watchElementBoundary(el.parentElement!)
+ .pipe(
+ filter(Boolean)
+ )
+
+ /* Retrieve container */
+ const container = el.parentElement!
+
+ /* Retrieve nested components */
+ const meta = getElement(":scope > :first-child", el)
+ const list = getElement(":scope > :last-child", el)
+
+ /* Reveal to accessibility tree – see https://bit.ly/3iAA7t8 */
+ watchToggle("search")
+ .subscribe(active => list.setAttribute(
+ "role", active ? "list" : "presentation"
+ ))
+
+ /* Update search result metadata */
+ push$
+ .pipe(
+ withLatestFrom(query$),
+ skipUntil(worker$.pipe(first(isSearchReadyMessage)))
+ )
+ .subscribe(([{ items }, { value }]) => {
+ switch (items.length) {
+
+ /* No results */
+ case 0:
+ meta.textContent = value.length
+ ? translation("search.result.none")
+ : translation("search.result.placeholder")
+ break
+
+ /* One result */
+ case 1:
+ meta.textContent = translation("search.result.one")
+ break
+
+ /* Multiple result */
+ default:
+ const count = round(items.length)
+ meta.textContent = translation("search.result.other", count)
+ }
+ })
+
+ /* Render search result item */
+ const render$ = push$
+ .pipe(
+ tap(() => list.innerHTML = ""),
+ switchMap(({ items }) => merge(
+ of(...items.slice(0, 10)),
+ of(...items.slice(10))
+ .pipe(
+ bufferCount(4),
+ zipWith(boundary$),
+ switchMap(([chunk]) => chunk)
+ )
+ )),
+ map(renderSearchResultItem),
+ share()
+ )
+
+ /* Update search result list */
+ render$.subscribe(item => list.appendChild(item))
+ render$
+ .pipe(
+ mergeMap(item => {
+ const details = getOptionalElement("details", item)
+ if (typeof details === "undefined")
+ return EMPTY
+
+ /* Keep position of details element stable */
+ return fromEvent(details, "toggle")
+ .pipe(
+ takeUntil(push$),
+ map(() => details)
+ )
+ })
+ )
+ .subscribe(details => {
+ if (
+ details.open === false &&
+ details.offsetTop <= container.scrollTop
+ )
+ container.scrollTo({ top: details.offsetTop })
+ })
+
+ /* Filter search result message */
+ const result$ = worker$
+ .pipe(
+ filter(isSearchResultMessage),
+ map(({ data }) => data)
+ )
+
+ /* Create and return component */
+ return result$
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+}
diff --git a/src/templates/assets/javascripts/components/search/share/index.ts b/src/templates/assets/javascripts/components/search/share/index.ts
new file mode 100644
index 00000000..3db382c8
--- /dev/null
+++ b/src/templates/assets/javascripts/components/search/share/index.ts
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ endWith,
+ finalize,
+ fromEvent,
+ ignoreElements,
+ map,
+ takeUntil,
+ tap
+} from "rxjs"
+
+import { getLocation } from "~/browser"
+
+import { Component } from "../../_"
+import { SearchQuery } from "../query"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search sharing
+ */
+export interface SearchShare {
+ url: URL /* Deep link for sharing */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ query$: Observable<SearchQuery> /* Search query observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ query$: Observable<SearchQuery> /* Search query observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount search sharing
+ *
+ * @param _el - Search sharing element
+ * @param options - Options
+ *
+ * @returns Search sharing observable
+ */
+export function watchSearchShare(
+ _el: HTMLElement, { query$ }: WatchOptions
+): Observable<SearchShare> {
+ return query$
+ .pipe(
+ map(({ value }) => {
+ const url = getLocation()
+ url.hash = ""
+
+ /* Compute readable query strings */
+ value = value
+ .replace(/\s+/g, "+") /* Collapse whitespace */
+ .replace(/&/g, "%26") /* Escape '&' character */
+ .replace(/=/g, "%3D") /* Escape '=' character */
+
+ /* Replace query string */
+ url.search = `q=${value}`
+ return { url }
+ })
+ )
+}
+
+/**
+ * Mount search sharing
+ *
+ * @param el - Search sharing element
+ * @param options - Options
+ *
+ * @returns Search sharing component observable
+ */
+export function mountSearchShare(
+ el: HTMLAnchorElement, options: MountOptions
+): Observable<Component<SearchShare>> {
+ const push$ = new Subject<SearchShare>()
+ const done$ = push$.pipe(ignoreElements(), endWith(true))
+ push$.subscribe(({ url }) => {
+ el.setAttribute("data-clipboard-text", el.href)
+ el.href = `${url}`
+ })
+
+ /* Prevent following of link */
+ fromEvent(el, "click")
+ .pipe(
+ takeUntil(done$)
+ )
+ .subscribe(ev => ev.preventDefault())
+
+ /* Create and return component */
+ return watchSearchShare(el, options)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+}
diff --git a/src/templates/assets/javascripts/components/search/suggest/index.ts b/src/templates/assets/javascripts/components/search/suggest/index.ts
new file mode 100644
index 00000000..e7881475
--- /dev/null
+++ b/src/templates/assets/javascripts/components/search/suggest/index.ts
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ asyncScheduler,
+ combineLatestWith,
+ distinctUntilChanged,
+ filter,
+ finalize,
+ fromEvent,
+ map,
+ merge,
+ observeOn,
+ tap
+} from "rxjs"
+
+import { Keyboard } from "~/browser"
+import {
+ SearchMessage,
+ SearchResult,
+ isSearchResultMessage
+} from "~/integrations"
+
+import { Component, getComponentElement } from "../../_"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search suggestions
+ */
+export interface SearchSuggest {}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ keyboard$: Observable<Keyboard> /* Keyboard observable */
+ worker$: Subject<SearchMessage> /* Search worker */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Mount search suggestions
+ *
+ * This function will perform a lazy rendering of the search results, depending
+ * on the vertical offset of the search result container.
+ *
+ * @param el - Search result list element
+ * @param options - Options
+ *
+ * @returns Search result list component observable
+ */
+export function mountSearchSuggest(
+ el: HTMLElement, { worker$, keyboard$ }: MountOptions
+): Observable<Component<SearchSuggest>> {
+ const push$ = new Subject<SearchResult>()
+
+ /* Retrieve query component and track all changes */
+ const query = getComponentElement("search-query")
+ const query$ = merge(
+ fromEvent(query, "keydown"),
+ fromEvent(query, "focus")
+ )
+ .pipe(
+ observeOn(asyncScheduler),
+ map(() => query.value),
+ distinctUntilChanged(),
+ )
+
+ /* Update search suggestions */
+ push$
+ .pipe(
+ combineLatestWith(query$),
+ map(([{ suggest }, value]) => {
+ const words = value.split(/([\s-]+)/)
+ if (suggest?.length && words[words.length - 1]) {
+ const last = suggest[suggest.length - 1]
+ if (last.startsWith(words[words.length - 1]))
+ words[words.length - 1] = last
+ } else {
+ words.length = 0
+ }
+ return words
+ })
+ )
+ .subscribe(words => el.innerHTML = words
+ .join("")
+ .replace(/\s/g, "&nbsp;")
+ )
+
+ /* Set up search keyboard handlers */
+ keyboard$
+ .pipe(
+ filter(({ mode }) => mode === "search")
+ )
+ .subscribe(key => {
+ switch (key.type) {
+
+ /* Right arrow: accept current suggestion */
+ case "ArrowRight":
+ if (
+ el.innerText.length &&
+ query.selectionStart === query.value.length
+ )
+ query.value = el.innerText
+ break
+ }
+ })
+
+ /* Filter search result message */
+ const result$ = worker$
+ .pipe(
+ filter(isSearchResultMessage),
+ map(({ data }) => data)
+ )
+
+ /* Create and return component */
+ return result$
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(() => ({ ref: el }))
+ )
+}
diff --git a/src/templates/assets/javascripts/components/sidebar/index.ts b/src/templates/assets/javascripts/components/sidebar/index.ts
new file mode 100644
index 00000000..82f3d03e
--- /dev/null
+++ b/src/templates/assets/javascripts/components/sidebar/index.ts
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ animationFrameScheduler,
+ asyncScheduler,
+ auditTime,
+ combineLatest,
+ defer,
+ distinctUntilChanged,
+ endWith,
+ finalize,
+ first,
+ from,
+ fromEvent,
+ ignoreElements,
+ map,
+ mergeMap,
+ observeOn,
+ takeUntil,
+ tap,
+ withLatestFrom
+} from "rxjs"
+
+import {
+ Viewport,
+ getElement,
+ getElementContainer,
+ getElementOffset,
+ getElementSize,
+ getElements
+} from "~/browser"
+
+import { Component } from "../_"
+import { Header } from "../header"
+import { Main } from "../main"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Sidebar
+ */
+export interface Sidebar {
+ height: number /* Sidebar height */
+ locked: boolean /* Sidebar is locked */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ main$: Observable<Main> /* Main area observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+ main$: Observable<Main> /* Main area observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch sidebar
+ *
+ * This function returns an observable that computes the visual parameters of
+ * the sidebar which depends on the vertical viewport offset, as well as the
+ * height of the main area. When the page is scrolled beyond the header, the
+ * sidebar is locked and fills the remaining space.
+ *
+ * @param el - Sidebar element
+ * @param options - Options
+ *
+ * @returns Sidebar observable
+ */
+export function watchSidebar(
+ el: HTMLElement, { viewport$, main$ }: WatchOptions
+): Observable<Sidebar> {
+ const parent = el.closest<HTMLElement>(".md-grid")!
+ const adjust =
+ parent.offsetTop -
+ parent.parentElement!.offsetTop
+
+ /* Compute the sidebar's available height and if it should be locked */
+ return combineLatest([main$, viewport$])
+ .pipe(
+ map(([{ offset, height }, { offset: { y } }]) => {
+ height = height
+ + Math.min(adjust, Math.max(0, y - offset))
+ - adjust
+ return {
+ height,
+ locked: y >= offset + adjust
+ }
+ }),
+ distinctUntilChanged((a, b) => (
+ a.height === b.height &&
+ a.locked === b.locked
+ ))
+ )
+}
+
+/**
+ * Mount sidebar
+ *
+ * This function doesn't set the height of the actual sidebar, but of its first
+ * child – the `.md-sidebar__scrollwrap` element in order to mitigiate jittery
+ * sidebars when the footer is scrolled into view. At some point we switched
+ * from `absolute` / `fixed` positioning to `sticky` positioning, significantly
+ * reducing jitter in some browsers (respectively Firefox and Safari) when
+ * scrolling from the top. However, top-aligned sticky positioning means that
+ * the sidebar snaps to the bottom when the end of the container is reached.
+ * This is what leads to the mentioned jitter, as the sidebar's height may be
+ * updated too slowly.
+ *
+ * This behaviour can be mitigiated by setting the height of the sidebar to `0`
+ * while preserving the padding, and the height on its first element.
+ *
+ * @param el - Sidebar element
+ * @param options - Options
+ *
+ * @returns Sidebar component observable
+ */
+export function mountSidebar(
+ el: HTMLElement, { header$, ...options }: MountOptions
+): Observable<Component<Sidebar>> {
+ const inner = getElement(".md-sidebar__scrollwrap", el)
+ const { y } = getElementOffset(inner)
+ return defer(() => {
+ const push$ = new Subject<Sidebar>()
+ const done$ = push$.pipe(ignoreElements(), endWith(true))
+ const next$ = push$
+ .pipe(
+ auditTime(0, animationFrameScheduler)
+ )
+
+ /* Update sidebar height and offset */
+ next$.pipe(withLatestFrom(header$))
+ .subscribe({
+
+ /* Handle emission */
+ next([{ height }, { height: offset }]) {
+ inner.style.height = `${height - 2 * y}px`
+ el.style.top = `${offset}px`
+ },
+
+ /* Handle complete */
+ complete() {
+ inner.style.height = ""
+ el.style.top = ""
+ }
+ })
+
+ /* Bring active item into view on initial load */
+ next$.pipe(first())
+ .subscribe(() => {
+ for (const item of getElements(".md-nav__link--active[href]", el)) {
+ const container = getElementContainer(item)
+ if (typeof container !== "undefined") {
+ const offset = item.offsetTop - container.offsetTop
+ const { height } = getElementSize(container)
+ container.scrollTo({
+ top: offset - height / 2
+ })
+ }
+ }
+ })
+
+ /* Handle accessibility for expandable items, see https://bit.ly/3jaod9p */
+ from(getElements<HTMLLabelElement>("label[tabindex]", el))
+ .pipe(
+ mergeMap(label => fromEvent(label, "click")
+ .pipe(
+ observeOn(asyncScheduler),
+ map(() => label),
+ takeUntil(done$)
+ )
+ )
+ )
+ .subscribe(label => {
+ const input = getElement<HTMLInputElement>(`[id="${label.htmlFor}"]`)
+ const nav = getElement(`[aria-labelledby="${label.id}"]`)
+ nav.setAttribute("aria-expanded", `${input.checked}`)
+ })
+
+ /* Create and return component */
+ return watchSidebar(el, options)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/source/_/index.ts b/src/templates/assets/javascripts/components/source/_/index.ts
new file mode 100644
index 00000000..5f6c4d11
--- /dev/null
+++ b/src/templates/assets/javascripts/components/source/_/index.ts
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Observable,
+ Subject,
+ catchError,
+ defer,
+ filter,
+ finalize,
+ map,
+ of,
+ shareReplay,
+ tap
+} from "rxjs"
+
+import { getElement } from "~/browser"
+import { ConsentDefaults } from "~/components/consent"
+import { renderSourceFacts } from "~/templates"
+
+import {
+ Component,
+ getComponentElements
+} from "../../_"
+import {
+ SourceFacts,
+ fetchSourceFacts
+} from "../facts"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Repository information
+ */
+export interface Source {
+ facts: SourceFacts /* Repository facts */
+}
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Repository information observable
+ */
+let fetch$: Observable<Source>
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch repository information
+ *
+ * This function tries to read the repository facts from session storage, and
+ * if unsuccessful, fetches them from the underlying provider.
+ *
+ * @param el - Repository information element
+ *
+ * @returns Repository information observable
+ */
+export function watchSource(
+ el: HTMLAnchorElement
+): Observable<Source> {
+ return fetch$ ||= defer(() => {
+ const cached = __md_get<SourceFacts>("__source", sessionStorage)
+ if (cached) {
+ return of(cached)
+ } else {
+
+ /* Check if consent is configured and was given */
+ const els = getComponentElements("consent")
+ if (els.length) {
+ const consent = __md_get<ConsentDefaults>("__consent")
+ if (!(consent && consent.github))
+ return EMPTY
+ }
+
+ /* Fetch repository facts */
+ return fetchSourceFacts(el.href)
+ .pipe(
+ tap(facts => __md_set("__source", facts, sessionStorage))
+ )
+ }
+ })
+ .pipe(
+ catchError(() => EMPTY),
+ filter(facts => Object.keys(facts).length > 0),
+ map(facts => ({ facts })),
+ shareReplay(1)
+ )
+}
+
+/**
+ * Mount repository information
+ *
+ * @param el - Repository information element
+ *
+ * @returns Repository information component observable
+ */
+export function mountSource(
+ el: HTMLAnchorElement
+): Observable<Component<Source>> {
+ const inner = getElement(":scope > :last-child", el)
+ return defer(() => {
+ const push$ = new Subject<Source>()
+ push$.subscribe(({ facts }) => {
+ inner.appendChild(renderSourceFacts(facts))
+ inner.classList.add("md-source__repository--active")
+ })
+
+ /* Create and return component */
+ return watchSource(el)
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/source/facts/_/index.ts b/src/templates/assets/javascripts/components/source/facts/_/index.ts
new file mode 100644
index 00000000..154f229f
--- /dev/null
+++ b/src/templates/assets/javascripts/components/source/facts/_/index.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { EMPTY, Observable } from "rxjs"
+
+import { fetchSourceFactsFromGitHub } from "../github"
+import { fetchSourceFactsFromGitLab } from "../gitlab"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Repository facts for repositories
+ */
+export interface RepositoryFacts {
+ stars?: number /* Number of stars */
+ forks?: number /* Number of forks */
+ version?: string /* Latest version */
+}
+
+/**
+ * Repository facts for organizations
+ */
+export interface OrganizationFacts {
+ repositories?: number /* Number of repositories */
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Repository facts
+ */
+export type SourceFacts =
+ | RepositoryFacts
+ | OrganizationFacts
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Fetch repository facts
+ *
+ * @param url - Repository URL
+ *
+ * @returns Repository facts observable
+ */
+export function fetchSourceFacts(
+ url: string
+): Observable<SourceFacts> {
+
+ /* Try to match GitHub repository */
+ let match = url.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i)
+ if (match) {
+ const [, user, repo] = match
+ return fetchSourceFactsFromGitHub(user, repo)
+ }
+
+ /* Try to match GitLab repository */
+ match = url.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i)
+ if (match) {
+ const [, base, slug] = match
+ return fetchSourceFactsFromGitLab(base, slug)
+ }
+
+ /* Fallback */
+ return EMPTY
+}
diff --git a/src/templates/assets/javascripts/components/source/facts/github/index.ts b/src/templates/assets/javascripts/components/source/facts/github/index.ts
new file mode 100644
index 00000000..12cc55e0
--- /dev/null
+++ b/src/templates/assets/javascripts/components/source/facts/github/index.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { Repo, User } from "github-types"
+import {
+ EMPTY,
+ Observable,
+ catchError,
+ defaultIfEmpty,
+ map,
+ zip
+} from "rxjs"
+
+import { requestJSON } from "~/browser"
+
+import { SourceFacts } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * GitHub release (partial)
+ */
+interface Release {
+ tag_name: string /* Tag name */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Fetch GitHub repository facts
+ *
+ * @param user - GitHub user or organization
+ * @param repo - GitHub repository
+ *
+ * @returns Repository facts observable
+ */
+export function fetchSourceFactsFromGitHub(
+ user: string, repo?: string
+): Observable<SourceFacts> {
+ if (typeof repo !== "undefined") {
+ const url = `https://api.github.com/repos/${user}/${repo}`
+ return zip(
+
+ /* Fetch version */
+ requestJSON<Release>(`${url}/releases/latest`)
+ .pipe(
+ catchError(() => EMPTY), // @todo refactor instant loading
+ map(release => ({
+ version: release.tag_name
+ })),
+ defaultIfEmpty({})
+ ),
+
+ /* Fetch stars and forks */
+ requestJSON<Repo>(url)
+ .pipe(
+ catchError(() => EMPTY), // @todo refactor instant loading
+ map(info => ({
+ stars: info.stargazers_count,
+ forks: info.forks_count
+ })),
+ defaultIfEmpty({})
+ )
+ )
+ .pipe(
+ map(([release, info]) => ({ ...release, ...info }))
+ )
+
+ /* User or organization */
+ } else {
+ const url = `https://api.github.com/users/${user}`
+ return requestJSON<User>(url)
+ .pipe(
+ map(info => ({
+ repositories: info.public_repos
+ })),
+ defaultIfEmpty({})
+ )
+ }
+}
diff --git a/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts b/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts
new file mode 100644
index 00000000..d85d4afd
--- /dev/null
+++ b/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { ProjectSchema } from "gitlab"
+import {
+ EMPTY,
+ Observable,
+ catchError,
+ defaultIfEmpty,
+ map
+} from "rxjs"
+
+import { requestJSON } from "~/browser"
+
+import { SourceFacts } from "../_"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Fetch GitLab repository facts
+ *
+ * @param base - GitLab base
+ * @param project - GitLab project
+ *
+ * @returns Repository facts observable
+ */
+export function fetchSourceFactsFromGitLab(
+ base: string, project: string
+): Observable<SourceFacts> {
+ const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}`
+ return requestJSON<ProjectSchema>(url)
+ .pipe(
+ catchError(() => EMPTY), // @todo refactor instant loading
+ map(({ star_count, forks_count }) => ({
+ stars: star_count,
+ forks: forks_count
+ })),
+ defaultIfEmpty({})
+ )
+}
diff --git a/src/templates/assets/javascripts/components/source/facts/index.ts b/src/templates/assets/javascripts/components/source/facts/index.ts
new file mode 100644
index 00000000..f9bda64d
--- /dev/null
+++ b/src/templates/assets/javascripts/components/source/facts/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./github"
+export * from "./gitlab"
diff --git a/src/templates/assets/javascripts/components/source/index.ts b/src/templates/assets/javascripts/components/source/index.ts
new file mode 100644
index 00000000..7fac4813
--- /dev/null
+++ b/src/templates/assets/javascripts/components/source/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./facts"
diff --git a/src/templates/assets/javascripts/components/tabs/index.ts b/src/templates/assets/javascripts/components/tabs/index.ts
new file mode 100644
index 00000000..1e69df28
--- /dev/null
+++ b/src/templates/assets/javascripts/components/tabs/index.ts
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ defer,
+ distinctUntilKeyChanged,
+ finalize,
+ map,
+ of,
+ switchMap,
+ tap
+} from "rxjs"
+
+import { feature } from "~/_"
+import {
+ Viewport,
+ watchElementSize,
+ watchViewportAt
+} from "~/browser"
+
+import { Component } from "../_"
+import { Header } from "../header"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Navigation tabs
+ */
+export interface Tabs {
+ hidden: boolean /* Navigation tabs are hidden */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch navigation tabs
+ *
+ * @param el - Navigation tabs element
+ * @param options - Options
+ *
+ * @returns Navigation tabs observable
+ */
+export function watchTabs(
+ el: HTMLElement, { viewport$, header$ }: WatchOptions
+): Observable<Tabs> {
+ return watchElementSize(document.body)
+ .pipe(
+ switchMap(() => watchViewportAt(el, { header$, viewport$ })),
+ map(({ offset: { y } }) => {
+ return {
+ hidden: y >= 10
+ }
+ }),
+ distinctUntilKeyChanged("hidden")
+ )
+}
+
+/**
+ * Mount navigation tabs
+ *
+ * This function hides the navigation tabs when scrolling past the threshold
+ * and makes them reappear in a nice CSS animation when scrolling back up.
+ *
+ * @param el - Navigation tabs element
+ * @param options - Options
+ *
+ * @returns Navigation tabs component observable
+ */
+export function mountTabs(
+ el: HTMLElement, options: MountOptions
+): Observable<Component<Tabs>> {
+ return defer(() => {
+ const push$ = new Subject<Tabs>()
+ push$.subscribe({
+
+ /* Handle emission */
+ next({ hidden }) {
+ el.hidden = hidden
+ },
+
+ /* Handle complete */
+ complete() {
+ el.hidden = false
+ }
+ })
+
+ /* Create and return component */
+ return (
+ feature("navigation.tabs.sticky")
+ ? of({ hidden: false })
+ : watchTabs(el, options)
+ )
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/toc/index.ts b/src/templates/assets/javascripts/components/toc/index.ts
new file mode 100644
index 00000000..04b8d85f
--- /dev/null
+++ b/src/templates/assets/javascripts/components/toc/index.ts
@@ -0,0 +1,379 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ asyncScheduler,
+ bufferCount,
+ combineLatestWith,
+ debounceTime,
+ defer,
+ distinctUntilChanged,
+ distinctUntilKeyChanged,
+ endWith,
+ filter,
+ finalize,
+ ignoreElements,
+ map,
+ merge,
+ observeOn,
+ of,
+ repeat,
+ scan,
+ share,
+ skip,
+ startWith,
+ switchMap,
+ takeUntil,
+ tap,
+ withLatestFrom
+} from "rxjs"
+
+import { feature } from "~/_"
+import {
+ Viewport,
+ getElement,
+ getElementContainer,
+ getElementSize,
+ getElements,
+ getLocation,
+ getOptionalElement,
+ watchElementSize
+} from "~/browser"
+
+import {
+ Component,
+ getComponentElement
+} from "../_"
+import { Header } from "../header"
+import { Main } from "../main"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Table of contents
+ */
+export interface TableOfContents {
+ prev: HTMLAnchorElement[][] /* Anchors (previous) */
+ next: HTMLAnchorElement[][] /* Anchors (next) */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+ main$: Observable<Main> /* Main area observable */
+ target$: Observable<HTMLElement> /* Location target observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch table of contents
+ *
+ * This is effectively a scroll spy implementation which will account for the
+ * fixed header and automatically re-calculate anchor offsets when the viewport
+ * is resized. The returned observable will only emit if the table of contents
+ * needs to be repainted.
+ *
+ * This implementation tracks an anchor element's entire path starting from its
+ * level up to the top-most anchor element, e.g. `[h3, h2, h1]`. Although the
+ * Material theme currently doesn't make use of this information, it enables
+ * the styling of the entire hierarchy through customization.
+ *
+ * Note that the current anchor is the last item of the `prev` anchor list.
+ *
+ * @param el - Table of contents element
+ * @param options - Options
+ *
+ * @returns Table of contents observable
+ */
+export function watchTableOfContents(
+ el: HTMLElement, { viewport$, header$ }: WatchOptions
+): Observable<TableOfContents> {
+ const table = new Map<HTMLAnchorElement, HTMLElement>()
+
+ /* Compute anchor-to-target mapping */
+ const anchors = getElements<HTMLAnchorElement>("[href^=\\#]", el)
+ for (const anchor of anchors) {
+ const id = decodeURIComponent(anchor.hash.substring(1))
+ const target = getOptionalElement(`[id="${id}"]`)
+ if (typeof target !== "undefined")
+ table.set(anchor, target)
+ }
+
+ /* Compute necessary adjustment for header */
+ const adjust$ = header$
+ .pipe(
+ distinctUntilKeyChanged("height"),
+ map(({ height }) => {
+ const main = getComponentElement("main")
+ const grid = getElement(":scope > :first-child", main)
+ return height + 0.8 * (
+ grid.offsetTop -
+ main.offsetTop
+ )
+ }),
+ share()
+ )
+
+ /* Compute partition of previous and next anchors */
+ const partition$ = watchElementSize(document.body)
+ .pipe(
+ distinctUntilKeyChanged("height"),
+
+ /* Build index to map anchor paths to vertical offsets */
+ switchMap(body => defer(() => {
+ let path: HTMLAnchorElement[] = []
+ return of([...table].reduce((index, [anchor, target]) => {
+ while (path.length) {
+ const last = table.get(path[path.length - 1])!
+ if (last.tagName >= target.tagName) {
+ path.pop()
+ } else {
+ break
+ }
+ }
+
+ /* If the current anchor is hidden, continue with its parent */
+ let offset = target.offsetTop
+ while (!offset && target.parentElement) {
+ target = target.parentElement
+ offset = target.offsetTop
+ }
+
+ /* Fix anchor offsets in tables - see https://bit.ly/3CUFOcn */
+ let parent = target.offsetParent as HTMLElement
+ for (; parent; parent = parent.offsetParent as HTMLElement)
+ offset += parent.offsetTop
+
+ /* Map reversed anchor path to vertical offset */
+ return index.set(
+ [...path = [...path, anchor]].reverse(),
+ offset
+ )
+ }, new Map<HTMLAnchorElement[], number>()))
+ })
+ .pipe(
+
+ /* Sort index by vertical offset (see https://bit.ly/30z6QSO) */
+ map(index => new Map([...index].sort(([, a], [, b]) => a - b))),
+ combineLatestWith(adjust$),
+
+ /* Re-compute partition when viewport offset changes */
+ switchMap(([index, adjust]) => viewport$
+ .pipe(
+ scan(([prev, next], { offset: { y }, size }) => {
+ const last = y + size.height >= Math.floor(body.height)
+
+ /* Look forward */
+ while (next.length) {
+ const [, offset] = next[0]
+ if (offset - adjust < y || last) {
+ prev = [...prev, next.shift()!]
+ } else {
+ break
+ }
+ }
+
+ /* Look backward */
+ while (prev.length) {
+ const [, offset] = prev[prev.length - 1]
+ if (offset - adjust >= y && !last) {
+ next = [prev.pop()!, ...next]
+ } else {
+ break
+ }
+ }
+
+ /* Return partition */
+ return [prev, next]
+ }, [[], [...index]]),
+ distinctUntilChanged((a, b) => (
+ a[0] === b[0] &&
+ a[1] === b[1]
+ ))
+ )
+ )
+ )
+ )
+ )
+
+ /* Compute and return anchor list migrations */
+ return partition$
+ .pipe(
+ map(([prev, next]) => ({
+ prev: prev.map(([path]) => path),
+ next: next.map(([path]) => path)
+ })),
+
+ /* Extract anchor list migrations */
+ startWith({ prev: [], next: [] }),
+ bufferCount(2, 1),
+ map(([a, b]) => {
+
+ /* Moving down */
+ if (a.prev.length < b.prev.length) {
+ return {
+ prev: b.prev.slice(Math.max(0, a.prev.length - 1), b.prev.length),
+ next: []
+ }
+
+ /* Moving up */
+ } else {
+ return {
+ prev: b.prev.slice(-1),
+ next: b.next.slice(0, b.next.length - a.next.length)
+ }
+ }
+ })
+ )
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Mount table of contents
+ *
+ * @param el - Table of contents element
+ * @param options - Options
+ *
+ * @returns Table of contents component observable
+ */
+export function mountTableOfContents(
+ el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions
+): Observable<Component<TableOfContents>> {
+ return defer(() => {
+ const push$ = new Subject<TableOfContents>()
+ const done$ = push$.pipe(ignoreElements(), endWith(true))
+ push$.subscribe(({ prev, next }) => {
+
+ /* Look forward */
+ for (const [anchor] of next) {
+ anchor.classList.remove("md-nav__link--passed")
+ anchor.classList.remove("md-nav__link--active")
+ }
+
+ /* Look backward */
+ for (const [index, [anchor]] of prev.entries()) {
+ anchor.classList.add("md-nav__link--passed")
+ anchor.classList.toggle(
+ "md-nav__link--active",
+ index === prev.length - 1
+ )
+ }
+ })
+
+ /* Set up following, if enabled */
+ if (feature("toc.follow")) {
+
+ /* Toggle smooth scrolling only for anchor clicks */
+ const smooth$ = merge(
+ viewport$.pipe(debounceTime(1), map(() => undefined)),
+ viewport$.pipe(debounceTime(250), map(() => "smooth" as const))
+ )
+
+ /* Bring active anchor into view */ // @todo: refactor
+ push$
+ .pipe(
+ filter(({ prev }) => prev.length > 0),
+ combineLatestWith(main$.pipe(observeOn(asyncScheduler))),
+ withLatestFrom(smooth$)
+ )
+ .subscribe(([[{ prev }], behavior]) => {
+ const [anchor] = prev[prev.length - 1]
+ if (anchor.offsetHeight) {
+
+ /* Retrieve overflowing container and scroll */
+ const container = getElementContainer(anchor)
+ if (typeof container !== "undefined") {
+ const offset = anchor.offsetTop - container.offsetTop
+ const { height } = getElementSize(container)
+ container.scrollTo({
+ top: offset - height / 2,
+ behavior
+ })
+ }
+ }
+ })
+ }
+
+ /* Set up anchor tracking, if enabled */
+ if (feature("navigation.tracking"))
+ viewport$
+ .pipe(
+ takeUntil(done$),
+ distinctUntilKeyChanged("offset"),
+ debounceTime(250),
+ skip(1),
+ takeUntil(target$.pipe(skip(1))),
+ repeat({ delay: 250 }),
+ withLatestFrom(push$)
+ )
+ .subscribe(([, { prev }]) => {
+ const url = getLocation()
+
+ /* Set hash fragment to active anchor */
+ const anchor = prev[prev.length - 1]
+ if (anchor && anchor.length) {
+ const [active] = anchor
+ const { hash } = new URL(active.href)
+ if (url.hash !== hash) {
+ url.hash = hash
+ history.replaceState({}, "", `${url}`)
+ }
+
+ /* Reset anchor when at the top */
+ } else {
+ url.hash = ""
+ history.replaceState({}, "", `${url}`)
+ }
+ })
+
+ /* Create and return component */
+ return watchTableOfContents(el, { viewport$, header$ })
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+ })
+}
diff --git a/src/templates/assets/javascripts/components/top/index.ts b/src/templates/assets/javascripts/components/top/index.ts
new file mode 100644
index 00000000..82e88b61
--- /dev/null
+++ b/src/templates/assets/javascripts/components/top/index.ts
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ Subject,
+ bufferCount,
+ combineLatest,
+ distinctUntilChanged,
+ distinctUntilKeyChanged,
+ endWith,
+ finalize,
+ fromEvent,
+ ignoreElements,
+ map,
+ repeat,
+ skip,
+ takeUntil,
+ tap
+} from "rxjs"
+
+import { Viewport } from "~/browser"
+
+import { Component } from "../_"
+import { Header } from "../header"
+import { Main } from "../main"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Back-to-top button
+ */
+export interface BackToTop {
+ hidden: boolean /* Back-to-top button is hidden */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch options
+ */
+interface WatchOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ main$: Observable<Main> /* Main area observable */
+ target$: Observable<HTMLElement> /* Location target observable */
+}
+
+/**
+ * Mount options
+ */
+interface MountOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ header$: Observable<Header> /* Header observable */
+ main$: Observable<Main> /* Main area observable */
+ target$: Observable<HTMLElement> /* Location target observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Watch back-to-top
+ *
+ * @param _el - Back-to-top element
+ * @param options - Options
+ *
+ * @returns Back-to-top observable
+ */
+export function watchBackToTop(
+ _el: HTMLElement, { viewport$, main$, target$ }: WatchOptions
+): Observable<BackToTop> {
+
+ /* Compute direction */
+ const direction$ = viewport$
+ .pipe(
+ map(({ offset: { y } }) => y),
+ bufferCount(2, 1),
+ map(([a, b]) => a > b && b > 0),
+ distinctUntilChanged()
+ )
+
+ /* Compute whether main area is active */
+ const active$ = main$
+ .pipe(
+ map(({ active }) => active)
+ )
+
+ /* Compute threshold for hiding */
+ return combineLatest([active$, direction$])
+ .pipe(
+ map(([active, direction]) => !(active && direction)),
+ distinctUntilChanged(),
+ takeUntil(target$.pipe(skip(1))),
+ endWith(true),
+ repeat({ delay: 250 }),
+ map(hidden => ({ hidden }))
+ )
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Mount back-to-top
+ *
+ * @param el - Back-to-top element
+ * @param options - Options
+ *
+ * @returns Back-to-top component observable
+ */
+export function mountBackToTop(
+ el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions
+): Observable<Component<BackToTop>> {
+ const push$ = new Subject<BackToTop>()
+ const done$ = push$.pipe(ignoreElements(), endWith(true))
+ push$.subscribe({
+
+ /* Handle emission */
+ next({ hidden }) {
+ el.hidden = hidden
+ if (hidden) {
+ el.setAttribute("tabindex", "-1")
+ el.blur()
+ } else {
+ el.removeAttribute("tabindex")
+ }
+ },
+
+ /* Handle complete */
+ complete() {
+ el.style.top = ""
+ el.hidden = true
+ el.removeAttribute("tabindex")
+ }
+ })
+
+ /* Watch header height */
+ header$
+ .pipe(
+ takeUntil(done$),
+ distinctUntilKeyChanged("height")
+ )
+ .subscribe(({ height }) => {
+ el.style.top = `${height + 16}px`
+ })
+
+ /* Go back to top */
+ fromEvent(el, "click")
+ .subscribe(ev => {
+ ev.preventDefault()
+ window.scrollTo({ top: 0 })
+ })
+
+ /* Create and return component */
+ return watchBackToTop(el, { viewport$, main$, target$ })
+ .pipe(
+ tap(state => push$.next(state)),
+ finalize(() => push$.complete()),
+ map(state => ({ ref: el, ...state }))
+ )
+}
diff --git a/src/templates/assets/javascripts/integrations/clipboard/index.ts b/src/templates/assets/javascripts/integrations/clipboard/index.ts
new file mode 100644
index 00000000..cf46f601
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/clipboard/index.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import ClipboardJS from "clipboard"
+import {
+ Observable,
+ Subject,
+ map,
+ tap
+} from "rxjs"
+
+import { translation } from "~/_"
+import { getElement } from "~/browser"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Setup options
+ */
+interface SetupOptions {
+ alert$: Subject<string> /* Alert subject */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Extract text to copy
+ *
+ * @param el - HTML element
+ *
+ * @returns Extracted text
+ */
+function extract(el: HTMLElement): string {
+ el.setAttribute("data-md-copying", "")
+ const copy = el.closest("[data-copy]")
+ const text = copy
+ ? copy.getAttribute("data-copy")!
+ : el.innerText
+ el.removeAttribute("data-md-copying")
+ return text
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Set up Clipboard.js integration
+ *
+ * @param options - Options
+ */
+export function setupClipboardJS(
+ { alert$ }: SetupOptions
+): void {
+ if (ClipboardJS.isSupported()) {
+ new Observable<ClipboardJS.Event>(subscriber => {
+ new ClipboardJS("[data-clipboard-target], [data-clipboard-text]", {
+ text: el => (
+ el.getAttribute("data-clipboard-text")! ||
+ extract(getElement(
+ el.getAttribute("data-clipboard-target")!
+ ))
+ )
+ })
+ .on("success", ev => subscriber.next(ev))
+ })
+ .pipe(
+ tap(ev => {
+ const trigger = ev.trigger as HTMLElement
+ trigger.focus()
+ }),
+ map(() => translation("clipboard.copied"))
+ )
+ .subscribe(alert$)
+ }
+}
diff --git a/src/templates/assets/javascripts/integrations/index.ts b/src/templates/assets/javascripts/integrations/index.ts
new file mode 100644
index 00000000..5d91a9d5
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./clipboard"
+export * from "./instant"
+export * from "./search"
+export * from "./sitemap"
+export * from "./version"
diff --git a/src/templates/assets/javascripts/integrations/instant/.eslintrc b/src/templates/assets/javascripts/integrations/instant/.eslintrc
new file mode 100644
index 00000000..5adf108a
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/instant/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "rules": {
+ "no-self-assign": "off",
+ "no-null/no-null": "off"
+ }
+}
diff --git a/src/templates/assets/javascripts/integrations/instant/index.ts b/src/templates/assets/javascripts/integrations/instant/index.ts
new file mode 100644
index 00000000..d321b751
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/instant/index.ts
@@ -0,0 +1,446 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Observable,
+ Subject,
+ bufferCount,
+ catchError,
+ concat,
+ debounceTime,
+ distinctUntilKeyChanged,
+ endWith,
+ filter,
+ fromEvent,
+ ignoreElements,
+ map,
+ of,
+ sample,
+ share,
+ skip,
+ startWith,
+ switchMap,
+ take,
+ withLatestFrom
+} from "rxjs"
+
+import { configuration, feature } from "~/_"
+import {
+ Viewport,
+ getElement,
+ getElements,
+ getLocation,
+ getOptionalElement,
+ request,
+ setLocation,
+ setLocationHash
+} from "~/browser"
+import { getComponentElement } from "~/components"
+
+import { fetchSitemap } from "../sitemap"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Setup options
+ */
+interface SetupOptions {
+ location$: Subject<URL> // Location subject
+ viewport$: Observable<Viewport> // Viewport observable
+ progress$: Subject<number> // Progress suject
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Create a map of head elements for lookup and replacement
+ *
+ * @param head - Document head
+ *
+ * @returns Element map
+ */
+function lookup(head: HTMLHeadElement): Map<string, HTMLElement> {
+
+ // @todo When resolving URLs, we must make sure to use the correct base for
+ // resolution. The next time we refactor instant loading, we should use the
+ // location subject as a source, which is also used for anchor links tracking,
+ // but for now we just rely on canonical.
+ const canonical = getElement<HTMLLinkElement>("[rel=canonical]", head)
+ canonical.href = canonical.href.replace("//localhost:", "//127.0.0.1")
+
+ // Create tag map and index elements in head
+ const tags = new Map<string, HTMLElement>()
+ for (const el of getElements(":scope > *", head)) {
+ let html = el.outerHTML
+
+ // If the current element is a style sheet or script, we must resolve the
+ // URL relative to the current location and make it absolute, so it's easy
+ // to deduplicate it later on by comparing the outer HTML of tags. We must
+ // keep identical style sheets and scripts without replacing them.
+ for (const key of ["href", "src"]) {
+ const value = el.getAttribute(key)!
+ if (value === null)
+ continue
+
+ // Resolve URL relative to current location
+ const url = new URL(value, canonical.href)
+ const ref = el.cloneNode() as HTMLElement
+
+ // Set resolved URL and retrieve HTML for deduplication
+ ref.setAttribute(key, `${url}`)
+ html = ref.outerHTML
+ break
+ }
+
+ // Index element in tag map
+ tags.set(html, el)
+ }
+
+ // Return tag map
+ return tags
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Set up instant navigation
+ *
+ * This is a heavily orchestrated operation - see inline comments to learn how
+ * this works with Material for MkDocs, and how you can hook into it.
+ *
+ * @param options - Options
+ *
+ * @returns Document observable
+ */
+export function setupInstantNavigation(
+ { location$, viewport$, progress$ }: SetupOptions
+): Observable<Document> {
+ const config = configuration()
+ if (location.protocol === "file:")
+ return EMPTY
+
+ // Load sitemap immediately, so we have it available when the user initiates
+ // the first instant navigation request, and canonicalize URLs to the current
+ // base URL. The base URL will remain stable in between loads, as it's only
+ // read at the first initialization of the application.
+ const sitemap$ = fetchSitemap()
+ .pipe(
+ map(paths => paths.map(path => `${new URL(path, config.base)}`))
+ )
+
+ // Intercept inter-site navigation - to keep the number of event listeners
+ // low we use the fact that uncaptured events bubble up to the body. This also
+ // has the nice property that we don't need to detach and then again attach
+ // event listeners when instant navigation occurs.
+ const instant$ = fromEvent<MouseEvent>(document.body, "click")
+ .pipe(
+ withLatestFrom(sitemap$),
+ switchMap(([ev, sitemap]) => {
+ if (!(ev.target instanceof Element))
+ return EMPTY
+
+ // Skip, as target is not within a link - clicks on non-link elements
+ // are also captured, which we need to exclude from processing
+ const el = ev.target.closest("a")
+ if (el === null)
+ return EMPTY
+
+ // Skip, as link opens in new window - we now know we have captured a
+ // click on a link, but the link either has a `target` property defined,
+ // or the user pressed the `meta` or `ctrl` key to open it in a new
+ // window. Thus, we need to filter those events, too.
+ if (el.target || ev.metaKey || ev.ctrlKey)
+ return EMPTY
+
+ // Next, we must check if the URL is relevant for us, i.e., if it's an
+ // internal link to a page that is managed by MkDocs. Only then we can
+ // be sure that the structure of the page to be loaded adheres to the
+ // current document structure and can subsequently be injected into it
+ // without doing a full reload. For this reason, we must canonicalize
+ // the URL by removing all search parameters and hash fragments.
+ const url = new URL(el.href)
+ url.search = url.hash = ""
+
+ // Skip, if URL is not included in the sitemap - this could be the case
+ // when linking between versions or languages, or to another page that
+ // the author included as part of the build, but that is not managed by
+ // MkDocs. In that case we must not continue with instant navigation.
+ if (!sitemap.includes(`${url}`))
+ return EMPTY
+
+ // We now know that we have a link to an internal page, so we prevent
+ // the browser from navigation and emit the URL for instant navigation.
+ // Note that this also includes anchor links, which means we need to
+ // implement anchor positioning ourselves. The reason for this is that
+ // if we wouldn't manage anchor links as well, scroll restoration will
+ // not work correctly (e.g. following an anchor link and scrolling).
+ ev.preventDefault()
+ return of(new URL(el.href))
+ }),
+ share()
+ )
+
+ // Before fetching for the first time, resolve the absolute favicon position,
+ // as the browser will try to fetch the icon immediately
+ instant$.pipe(take(1))
+ .subscribe(() => {
+ const favicon = getOptionalElement<HTMLLinkElement>("link[rel=icon]")
+ if (typeof favicon !== "undefined")
+ favicon.href = favicon.href
+ })
+
+ // Enable scroll restoration before window unloads - this is essential to
+ // ensure that full reloads (F5) restore the viewport offset correctly. If
+ // only popstate events wouldn't reset the scroll position prior to their
+ // emission, we could just reset this in popstate. Meh.
+ fromEvent(window, "beforeunload")
+ .subscribe(() => {
+ history.scrollRestoration = "auto"
+ })
+
+ // When an instant navigation event occurs, disable scroll restoration, since
+ // we must normalize and synchronize the behavior across all browsers. For
+ // instance, when the user clicks the back or forward button, the browser
+ // would immediately jump to the position of the previous document.
+ instant$.pipe(withLatestFrom(viewport$))
+ .subscribe(([url, { offset }]) => {
+ history.scrollRestoration = "manual"
+
+ // While it would be better UX to defer the history state change until the
+ // document was fully fetched and parsed, we must schedule it here, since
+ // popstate events are emitted when history state changes happen. Moreover
+ // we need to back up the current viewport offset, so we can restore it
+ // when popstate events occur, e.g., when the browser's back and forward
+ // buttons are used for navigation.
+ history.replaceState(offset, "")
+ history.pushState(null, "", url)
+ })
+
+ // Emit URL that should be fetched via instant navigation on location subject,
+ // which was passed into this function. Instant navigation can be intercepted
+ // by other parts of the application, which can synchronously back up or
+ // restore state before instant navigation happens.
+ instant$.subscribe(location$)
+
+ // Fetch document - when fetching, we could use `responseType: document`, but
+ // since all MkDocs links are relative, we need to make sure that the current
+ // location matches the document we just loaded. Otherwise any relative links
+ // in the document might use the old location. If the request fails for some
+ // reason, we fall back to regular navigation and set the location explicitly,
+ // which will force-load the page. Furthermore, we must pre-warm the buffer
+ // for the duplicate check, or the first click on an anchor link will also
+ // trigger an instant navigation event, which doesn't make sense.
+ const response$ = location$
+ .pipe(
+ startWith(getLocation()),
+ distinctUntilKeyChanged("pathname"),
+ skip(1),
+ switchMap(url => request(url, { progress$ })
+ .pipe(
+ catchError(() => {
+ setLocation(url, true)
+ return EMPTY
+ })
+ )
+ )
+ )
+
+ // Initialize the DOM parser, parse the returned HTML, and replace selected
+ // components before handing control down to the application
+ const dom = new DOMParser()
+ const document$ = response$
+ .pipe(
+ switchMap(res => res.text()),
+ switchMap(res => {
+ const next = dom.parseFromString(res, "text/html")
+ for (const selector of [
+ "[data-md-component=announce]",
+ "[data-md-component=container]",
+ "[data-md-component=header-topic]",
+ "[data-md-component=outdated]",
+ "[data-md-component=logo]",
+ "[data-md-component=skip]",
+ ...feature("navigation.tabs.sticky")
+ ? ["[data-md-component=tabs]"]
+ : []
+ ]) {
+ const source = getOptionalElement(selector)
+ const target = getOptionalElement(selector, next)
+ if (
+ typeof source !== "undefined" &&
+ typeof target !== "undefined"
+ ) {
+ source.replaceWith(target)
+ }
+ }
+
+ // Update meta tags
+ const source = lookup(document.head)
+ const target = lookup(next.head)
+ for (const [html, el] of target) {
+
+ // Hack: skip stylesheets and scripts until we manage to replace them
+ // entirely in order to omit flashes of white content @todo refactor
+ if (
+ el.getAttribute("rel") === "stylesheet" ||
+ el.hasAttribute("src")
+ )
+ continue
+
+ if (source.has(html)) {
+ source.delete(html)
+ } else {
+ document.head.appendChild(el)
+ }
+ }
+
+ // Remove meta tags that are not present in the new document
+ for (const el of source.values())
+
+ // Hack: skip stylesheets and scripts until we manage to replace them
+ // entirely in order to omit flashes of white content @todo refactor
+ if (
+ el.getAttribute("rel") === "stylesheet" ||
+ el.hasAttribute("src")
+ )
+ continue
+ else
+ el.remove()
+
+ // After components and meta tags were replaced, re-evaluate scripts
+ // that were provided by the author as part of Markdown files
+ const container = getComponentElement("container")
+ return concat(getElements("script", container))
+ .pipe(
+ switchMap(el => {
+ const script = next.createElement("script")
+ if (el.src) {
+ for (const name of el.getAttributeNames())
+ script.setAttribute(name, el.getAttribute(name)!)
+ el.replaceWith(script)
+
+ // Complete when script is loaded
+ return new Observable(observer => {
+ script.onload = () => observer.complete()
+ })
+
+ // Complete immediately
+ } else {
+ script.textContent = el.textContent
+ el.replaceWith(script)
+ return EMPTY
+ }
+ }),
+ ignoreElements(),
+ endWith(next)
+ )
+ }),
+ share()
+ )
+
+ // Intercept popstate events, e.g. when using the browser's back and forward
+ // buttons, and emit new location for fetching and parsing
+ const popstate$ = fromEvent<PopStateEvent>(window, "popstate")
+ popstate$.pipe(map(getLocation))
+ .subscribe(location$)
+
+ // Intercept clicks on anchor links, and scroll document into position - as
+ // we disabled scroll restoration, we need to do this manually here
+ location$
+ .pipe(
+ startWith(getLocation()),
+ bufferCount(2, 1),
+ filter(([prev, next]) => (
+ prev.pathname === next.pathname &&
+ prev.hash !== next.hash
+ )),
+ map(([, next]) => next)
+ )
+ .subscribe(url => {
+ if (history.state !== null || !url.hash) {
+ window.scrollTo(0, history.state?.y ?? 0)
+ } else {
+ history.scrollRestoration = "auto"
+ setLocationHash(url.hash)
+ history.scrollRestoration = "manual"
+ }
+ })
+
+ // Intercept clicks on the same anchor link - we must use a distinct pipeline
+ // for this, or we'd end up in a loop, setting the hash again and again
+ location$
+ .pipe(
+ sample(instant$),
+ startWith(getLocation()),
+ bufferCount(2, 1),
+ filter(([prev, next]) => (
+ prev.pathname === next.pathname &&
+ prev.hash === next.hash
+ )),
+ map(([, next]) => next)
+ )
+ .subscribe(url => {
+ history.scrollRestoration = "auto"
+ setLocationHash(url.hash)
+ history.scrollRestoration = "manual"
+
+ // Hack: we need to make sure that we don't end up with multiple history
+ // entries for the same anchor link, so we just remove the last entry
+ history.back()
+ })
+
+ // After parsing the document, check if the current history entry has a state.
+ // This may happen when users press the back or forward button to visit a page
+ // that was already seen. If there's no state, it means a new page was visited
+ // and we should scroll to the top, unless an anchor is given.
+ document$.pipe(withLatestFrom(location$))
+ .subscribe(([, url]) => {
+ if (history.state !== null || !url.hash) {
+ window.scrollTo(0, history.state?.y ?? 0)
+ } else {
+ setLocationHash(url.hash)
+ }
+ })
+
+ // If the current history is not empty, register an event listener updating
+ // the current history state whenever the scroll position changes. This must
+ // be debounced and cannot be done in popstate, as popstate has already
+ // removed the entry from the history.
+ viewport$
+ .pipe(
+ distinctUntilKeyChanged("offset"),
+ debounceTime(100)
+ )
+ .subscribe(({ offset }) => {
+ history.replaceState(offset, "")
+ })
+
+ // Return document
+ return document$
+}
diff --git a/src/templates/assets/javascripts/integrations/search/_/index.ts b/src/templates/assets/javascripts/integrations/search/_/index.ts
new file mode 100644
index 00000000..0e217fa4
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/_/index.ts
@@ -0,0 +1,332 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ SearchDocument,
+ SearchIndex,
+ SearchOptions,
+ setupSearchDocumentMap
+} from "../config"
+import {
+ Position,
+ PositionTable,
+ highlight,
+ highlightAll,
+ tokenize
+} from "../internal"
+import {
+ SearchQueryTerms,
+ getSearchQueryTerms,
+ parseSearchQuery,
+ segment,
+ transformSearchQuery
+} from "../query"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search item
+ */
+export interface SearchItem
+ extends SearchDocument
+{
+ score: number /* Score (relevance) */
+ terms: SearchQueryTerms /* Search query terms */
+}
+
+/**
+ * Search result
+ */
+export interface SearchResult {
+ items: SearchItem[][] /* Search items */
+ suggest?: string[] /* Search suggestions */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Create field extractor factory
+ *
+ * @param table - Position table map
+ *
+ * @returns Extractor factory
+ */
+function extractor(table: Map<string, PositionTable>) {
+ return (name: keyof SearchDocument) => {
+ return (doc: SearchDocument) => {
+ if (typeof doc[name] === "undefined")
+ return undefined
+
+ /* Compute identifier and initialize table */
+ const id = [doc.location, name].join(":")
+ table.set(id, lunr.tokenizer.table = [])
+
+ /* Return field value */
+ return doc[name]
+ }
+ }
+}
+
+/**
+ * Compute the difference of two lists of strings
+ *
+ * @param a - 1st list of strings
+ * @param b - 2nd list of strings
+ *
+ * @returns Difference
+ */
+function difference(a: string[], b: string[]): string[] {
+ const [x, y] = [new Set(a), new Set(b)]
+ return [
+ ...new Set([...x].filter(value => !y.has(value)))
+ ]
+}
+
+/* ----------------------------------------------------------------------------
+ * Class
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search index
+ */
+export class Search {
+
+ /**
+ * Search document map
+ */
+ protected map: Map<string, SearchDocument>
+
+ /**
+ * Search options
+ */
+ protected options: SearchOptions
+
+ /**
+ * The underlying Lunr.js search index
+ */
+ protected index: lunr.Index
+
+ /**
+ * Internal position table map
+ */
+ protected table: Map<string, PositionTable>
+
+ /**
+ * Create the search integration
+ *
+ * @param data - Search index
+ */
+ public constructor({ config, docs, options }: SearchIndex) {
+ const field = extractor(this.table = new Map())
+
+ /* Set up document map and options */
+ this.map = setupSearchDocumentMap(docs)
+ this.options = options
+
+ /* Set up document index */
+ this.index = lunr(function () {
+ this.metadataWhitelist = ["position"]
+ this.b(0)
+
+ /* Set up (multi-)language support */
+ if (config.lang.length === 1 && config.lang[0] !== "en") {
+ // @ts-expect-error - namespace indexing not supported
+ this.use(lunr[config.lang[0]])
+ } else if (config.lang.length > 1) {
+ this.use(lunr.multiLanguage(...config.lang))
+ }
+
+ /* Set up custom tokenizer (must be after language setup) */
+ this.tokenizer = tokenize as typeof lunr.tokenizer
+ lunr.tokenizer.separator = new RegExp(config.separator)
+
+ /* Set up custom segmenter, if loaded */
+ lunr.segmenter = "TinySegmenter" in lunr
+ ? new lunr.TinySegmenter()
+ : undefined
+
+ /* Compute functions to be removed from the pipeline */
+ const fns = difference([
+ "trimmer", "stopWordFilter", "stemmer"
+ ], config.pipeline)
+
+ /* Remove functions from the pipeline for registered languages */
+ for (const lang of config.lang.map(language => (
+ // @ts-expect-error - namespace indexing not supported
+ language === "en" ? lunr : lunr[language]
+ )))
+ for (const fn of fns) {
+ this.pipeline.remove(lang[fn])
+ this.searchPipeline.remove(lang[fn])
+ }
+
+ /* Set up index reference */
+ this.ref("location")
+
+ /* Set up index fields */
+ this.field("title", { boost: 1e3, extractor: field("title") })
+ this.field("text", { boost: 1e0, extractor: field("text") })
+ this.field("tags", { boost: 1e6, extractor: field("tags") })
+
+ /* Add documents to index */
+ for (const doc of docs)
+ this.add(doc, { boost: doc.boost })
+ })
+ }
+
+ /**
+ * Search for matching documents
+ *
+ * @param query - Search query
+ *
+ * @returns Search result
+ */
+ public search(query: string): SearchResult {
+
+ // Experimental Chinese segmentation
+ query = query.replace(/\p{sc=Han}+/gu, value => {
+ return [...segment(value, this.index.invertedIndex)]
+ .join("* ")
+ })
+
+ // @todo: move segmenter (above) into transformSearchQuery
+ query = transformSearchQuery(query)
+ if (!query)
+ return { items: [] }
+
+ /* Parse query to extract clauses for analysis */
+ const clauses = parseSearchQuery(query)
+ .filter(clause => (
+ clause.presence !== lunr.Query.presence.PROHIBITED
+ ))
+
+ /* Perform search and post-process results */
+ const groups = this.index.search(query)
+
+ /* Apply post-query boosts based on title and search query terms */
+ .reduce<SearchItem[]>((item, { ref, score, matchData }) => {
+ let doc = this.map.get(ref)
+ if (typeof doc !== "undefined") {
+
+ /* Shallow copy document */
+ doc = { ...doc }
+ if (doc.tags)
+ doc.tags = [...doc.tags]
+
+ /* Compute and analyze search query terms */
+ const terms = getSearchQueryTerms(
+ clauses,
+ Object.keys(matchData.metadata)
+ )
+
+ /* Highlight matches in fields */
+ for (const field of this.index.fields) {
+ if (typeof doc[field] === "undefined")
+ continue
+
+ /* Collect positions from matches */
+ const positions: Position[] = []
+ for (const match of Object.values(matchData.metadata))
+ if (typeof match[field] !== "undefined")
+ positions.push(...match[field].position)
+
+ /* Skip highlighting, if no positions were collected */
+ if (!positions.length)
+ continue
+
+ /* Load table and determine highlighting method */
+ const table = this.table.get([doc.location, field].join(":"))!
+ const fn = Array.isArray(doc[field])
+ ? highlightAll
+ : highlight
+
+ // @ts-expect-error - stop moaning, TypeScript!
+ doc[field] = fn(doc[field], table, positions, field !== "text")
+ }
+
+ /* Highlight title and text and apply post-query boosts */
+ const boost = +!doc.parent +
+ Object.values(terms)
+ .filter(t => t).length /
+ Object.keys(terms).length
+
+ /* Append item */
+ item.push({
+ ...doc,
+ score: score * (1 + boost ** 2),
+ terms
+ })
+ }
+ return item
+ }, [])
+
+ /* Sort search results again after applying boosts */
+ .sort((a, b) => b.score - a.score)
+
+ /* Group search results by article */
+ .reduce((items, result) => {
+ const doc = this.map.get(result.location)
+ if (typeof doc !== "undefined") {
+ const ref = doc.parent
+ ? doc.parent.location
+ : doc.location
+ items.set(ref, [...items.get(ref) || [], result])
+ }
+ return items
+ }, new Map<string, SearchItem[]>())
+
+ /* Ensure that every item set has an article */
+ for (const [ref, items] of groups)
+ if (!items.find(item => item.location === ref)) {
+ const doc = this.map.get(ref)!
+ items.push({ ...doc, score: 0, terms: {} })
+ }
+
+ /* Generate search suggestions, if desired */
+ let suggest: string[] | undefined
+ if (this.options.suggest) {
+ const titles = this.index.query(builder => {
+ for (const clause of clauses)
+ builder.term(clause.term, {
+ fields: ["title"],
+ presence: lunr.Query.presence.REQUIRED,
+ wildcard: lunr.Query.wildcard.TRAILING
+ })
+ })
+
+ /* Retrieve suggestions for best match */
+ suggest = titles.length
+ ? Object.keys(titles[0].matchData.metadata)
+ : []
+ }
+
+ /* Return search result */
+ return {
+ items: [...groups.values()],
+ ...typeof suggest !== "undefined" && { suggest }
+ }
+ }
+}
diff --git a/src/templates/assets/javascripts/integrations/search/config/index.ts b/src/templates/assets/javascripts/integrations/search/config/index.ts
new file mode 100644
index 00000000..3d88d1c6
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/config/index.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search configuration
+ */
+export interface SearchConfig {
+ lang: string[] /* Search languages */
+ separator: string /* Search separator */
+ pipeline: SearchPipelineFn[] /* Search pipeline */
+}
+
+/**
+ * Search document
+ */
+export interface SearchDocument {
+ location: string /* Document location */
+ title: string /* Document title */
+ text: string /* Document text */
+ tags?: string[] /* Document tags */
+ boost?: number /* Document boost */
+ parent?: SearchDocument /* Document parent */
+}
+
+/**
+ * Search options
+ */
+export interface SearchOptions {
+ suggest: boolean /* Search suggestions */
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Search index
+ */
+export interface SearchIndex {
+ config: SearchConfig /* Search configuration */
+ docs: SearchDocument[] /* Search documents */
+ options: SearchOptions /* Search options */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search pipeline function
+ */
+type SearchPipelineFn =
+ | "trimmer" /* Trimmer */
+ | "stopWordFilter" /* Stop word filter */
+ | "stemmer" /* Stemmer */
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Create a search document map
+ *
+ * This function creates a mapping of URLs (including anchors) to the actual
+ * articles and sections. It relies on the invariant that the search index is
+ * ordered with the main article appearing before all sections with anchors.
+ * If this is not the case, the logic music be changed.
+ *
+ * @param docs - Search documents
+ *
+ * @returns Search document map
+ */
+export function setupSearchDocumentMap(
+ docs: SearchDocument[]
+): Map<string, SearchDocument> {
+ const map = new Map<string, SearchDocument>()
+ for (const doc of docs) {
+ const [path] = doc.location.split("#")
+
+ /* Add document article */
+ const article = map.get(path)
+ if (typeof article === "undefined") {
+ map.set(path, doc)
+
+ /* Add document section */
+ } else {
+ map.set(doc.location, doc)
+ doc.parent = article
+ }
+ }
+
+ /* Return search document map */
+ return map
+}
diff --git a/src/templates/assets/javascripts/integrations/search/highlighter/index.ts b/src/templates/assets/javascripts/integrations/search/highlighter/index.ts
new file mode 100644
index 00000000..0fcbb19e
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/highlighter/index.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import escapeHTML from "escape-html"
+
+import { SearchConfig } from "../config"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search highlight function
+ *
+ * @param value - Value
+ *
+ * @returns Highlighted value
+ */
+export type SearchHighlightFn = (value: string) => string
+
+/**
+ * Search highlight factory function
+ *
+ * @param query - Query value
+ *
+ * @returns Search highlight function
+ */
+export type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Create a search highlighter
+ *
+ * @param config - Search configuration
+ *
+ * @returns Search highlight factory function
+ */
+export function setupSearchHighlighter(
+ config: SearchConfig
+): SearchHighlightFactoryFn {
+ // Hack: temporarily remove pure lookaheads and lookbehinds
+ const regex = config.separator.split("|").map(term => {
+ const temp = term.replace(/(\(\?[!=<][^)]+\))/g, "")
+ return temp.length === 0 ? "�" : term
+ })
+ .join("|")
+
+ const separator = new RegExp(regex, "img")
+ const highlight = (_: unknown, data: string, term: string) => {
+ return `${data}<mark data-md-highlight>${term}</mark>`
+ }
+
+ /* Return factory function */
+ return (query: string) => {
+ query = query
+ .replace(/[\s*+\-:~^]+/g, " ")
+ .trim()
+
+ /* Create search term match expression */
+ const match = new RegExp(`(^|${config.separator}|)(${
+ query
+ .replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&")
+ .replace(separator, "|")
+ })`, "img")
+
+ /* Highlight string value */
+ return value => escapeHTML(value)
+ .replace(match, highlight)
+ .replace(/<\/mark>(\s+)<mark[^>]*>/img, "$1")
+ }
+}
diff --git a/src/templates/assets/javascripts/integrations/search/index.ts b/src/templates/assets/javascripts/integrations/search/index.ts
new file mode 100644
index 00000000..94c010bb
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./config"
+export * from "./highlighter"
+export * from "./query"
+export * from "./worker"
diff --git a/src/templates/assets/javascripts/integrations/search/internal/.eslintrc b/src/templates/assets/javascripts/integrations/search/internal/.eslintrc
new file mode 100644
index 00000000..9368ceb6
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/internal/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "rules": {
+ "no-fallthrough": "off",
+ "no-underscore-dangle": "off"
+ }
+}
diff --git a/src/templates/assets/javascripts/integrations/search/internal/_/index.ts b/src/templates/assets/javascripts/integrations/search/internal/_/index.ts
new file mode 100644
index 00000000..ae8f6104
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/internal/_/index.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Visitor function
+ *
+ * @param start - Start offset
+ * @param end - End offset
+ */
+type VisitorFn = (
+ start: number, end: number
+) => void
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Split a string using the given separator
+ *
+ * @param input - Input value
+ * @param separator - Separator
+ * @param fn - Visitor function
+ */
+export function split(
+ input: string, separator: RegExp, fn: VisitorFn
+): void {
+ separator = new RegExp(separator, "g")
+
+ /* Split string using separator */
+ let match: RegExpExecArray | null
+ let index = 0
+ do {
+ match = separator.exec(input)
+
+ /* Emit non-empty range */
+ const until = match?.index ?? input.length
+ if (index < until)
+ fn(index, until)
+
+ /* Update last index */
+ if (match) {
+ const [term] = match
+ index = match.index + term.length
+
+ /* Support zero-length lookaheads */
+ if (term.length === 0)
+ separator.lastIndex = match.index + 1
+ }
+ } while (match)
+}
diff --git a/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts b/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts
new file mode 100644
index 00000000..2a98b9e1
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Extraction type
+ *
+ * This type defines the possible values that are encoded into the first two
+ * bits of a section that is part of the blocks of a tokenization table. There
+ * are three types of interest: HTML opening and closing tags, as well as the
+ * actual text content we need to extract for indexing.
+ */
+export const enum Extract {
+ TAG_OPEN = 0, /* HTML opening tag */
+ TEXT = 1, /* Text content */
+ TAG_CLOSE = 2 /* HTML closing tag */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Visitor function
+ *
+ * @param block - Block index
+ * @param type - Extraction type
+ * @param start - Start offset
+ * @param end - End offset
+ */
+type VisitorFn = (
+ block: number, type: Extract, start: number, end: number
+) => void
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Split a string into markup and text sections
+ *
+ * This function scans a string and divides it up into sections of markup and
+ * text. For each section, it invokes the given visitor function with the block
+ * index, extraction type, as well as start and end offsets. Using a visitor
+ * function (= streaming data) is ideal for minimizing pressure on the GC.
+ *
+ * @param input - Input value
+ * @param fn - Visitor function
+ */
+export function extract(
+ input: string, fn: VisitorFn
+): void {
+
+ let block = 0 /* Current block */
+ let start = 0 /* Current start offset */
+ let end = 0 /* Current end offset */
+
+ /* Split string into sections */
+ for (let stack = 0; end < input.length; end++) {
+
+ /* Opening tag after non-empty section */
+ if (input.charAt(end) === "<" && end > start) {
+ fn(block, Extract.TEXT, start, start = end)
+
+ /* Closing tag */
+ } else if (input.charAt(end) === ">") {
+ if (input.charAt(start + 1) === "/") {
+ if (--stack === 0)
+ fn(block++, Extract.TAG_CLOSE, start, end + 1)
+
+ /* Tag is not self-closing */
+ } else if (input.charAt(end - 1) !== "/") {
+ if (stack++ === 0)
+ fn(block, Extract.TAG_OPEN, start, end + 1)
+ }
+
+ /* New section */
+ start = end + 1
+ }
+ }
+
+ /* Add trailing section */
+ if (end > start)
+ fn(block, Extract.TEXT, start, end)
+}
diff --git a/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts b/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts
new file mode 100644
index 00000000..7cc3bf1a
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Position table
+ */
+export type PositionTable = number[][]
+
+/**
+ * Position
+ */
+export type Position = number
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Highlight all occurrences in a string
+ *
+ * This function receives a field's value (e.g. like `title` or `text`), it's
+ * position table that was generated during indexing, and the positions found
+ * when executing the query. It then highlights all occurrences, and returns
+ * their concatenation. In case of multiple blocks, two are returned.
+ *
+ * @param input - Input value
+ * @param table - Table for indexing
+ * @param positions - Occurrences
+ * @param full - Full results
+ *
+ * @returns Highlighted string value
+ */
+export function highlight(
+ input: string, table: PositionTable, positions: Position[], full = false
+): string {
+ return highlightAll([input], table, positions, full).pop()!
+}
+
+/**
+ * Highlight all occurrences in a set of strings
+ *
+ * @param inputs - Input values
+ * @param table - Table for indexing
+ * @param positions - Occurrences
+ * @param full - Full results
+ *
+ * @returns Highlighted string values
+ */
+export function highlightAll(
+ inputs: string[], table: PositionTable, positions: Position[], full = false
+): string[] {
+
+ /* Map blocks to input values */
+ const mapping = [0]
+ for (let t = 1; t < table.length; t++) {
+ const prev = table[t - 1]
+ const next = table[t]
+
+ /* Check if table points to new block */
+ const p = prev[prev.length - 1] >>> 2 & 0x3FF
+ const q = next[0] >>> 12
+
+ /* Add block to mapping */
+ mapping.push(+(p > q) + mapping[mapping.length - 1])
+ }
+
+ /* Highlight strings one after another */
+ return inputs.map((input, i) => {
+ let cursor = 0
+
+ /* Map occurrences to blocks */
+ const blocks = new Map<number, number[]>()
+ for (const p of positions.sort((a, b) => a - b)) {
+ const index = p & 0xFFFFF
+ const block = p >>> 20
+ if (mapping[block] !== i)
+ continue
+
+ /* Ensure presence of block group */
+ let group = blocks.get(block)
+ if (typeof group === "undefined")
+ blocks.set(block, group = [])
+
+ /* Add index to group */
+ group.push(index)
+ }
+
+ /* Just return string, if no occurrences */
+ if (blocks.size === 0)
+ return input
+
+ /* Compute slices */
+ const slices: string[] = []
+ for (const [block, indexes] of blocks) {
+ const t = table[block]
+
+ /* Extract positions and length */
+ const start = t[0] >>> 12
+ const end = t[t.length - 1] >>> 12
+ const length = t[t.length - 1] >>> 2 & 0x3FF
+
+ /* Add prefix, if full results are desired */
+ if (full && start > cursor)
+ slices.push(input.slice(cursor, start))
+
+ /* Extract and highlight slice */
+ let slice = input.slice(start, end + length)
+ for (const j of indexes.sort((a, b) => b - a)) {
+
+ /* Retrieve offset and length of match */
+ const p = (t[j] >>> 12) - start
+ const q = (t[j] >>> 2 & 0x3FF) + p
+
+ /* Wrap occurrence */
+ slice = [
+ slice.slice(0, p),
+ "<mark>",
+ slice.slice(p, q),
+ "</mark>",
+ slice.slice(q)
+ ].join("")
+ }
+
+ /* Update cursor */
+ cursor = end + length
+
+ /* Append slice and abort if we have two */
+ if (slices.push(slice) === 2)
+ break
+ }
+
+ /* Add suffix, if full results are desired */
+ if (full && cursor < input.length)
+ slices.push(input.slice(cursor))
+
+ /* Return highlighted slices */
+ return slices.join("")
+ })
+}
diff --git a/src/templates/assets/javascripts/integrations/search/internal/index.ts b/src/templates/assets/javascripts/integrations/search/internal/index.ts
new file mode 100644
index 00000000..c752329e
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/internal/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./extract"
+export * from "./highlight"
+export * from "./tokenize"
diff --git a/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts b/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts
new file mode 100644
index 00000000..f5089bc9
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { split } from "../_"
+import {
+ Extract,
+ extract
+} from "../extract"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Split a string or set of strings into tokens
+ *
+ * This tokenizer supersedes the default tokenizer that is provided by Lunr.js,
+ * as it is aware of HTML tags and allows for multi-character splitting.
+ *
+ * It takes the given inputs, splits each of them into markup and text sections,
+ * tokenizes and segments (if necessary) each of them, and then indexes them in
+ * a table by using a compact bit representation. Bitwise techniques are used
+ * to write and read from the table during indexing and querying.
+ *
+ * @see https://bit.ly/3W3Xw4J - Search: better, faster, smaller
+ *
+ * @param input - Input value(s)
+ *
+ * @returns Tokens
+ */
+export function tokenize(
+ input?: string | string[]
+): lunr.Token[] {
+ const tokens: lunr.Token[] = []
+ if (typeof input === "undefined")
+ return tokens
+
+ /* Tokenize strings one after another */
+ const inputs = Array.isArray(input) ? input : [input]
+ for (let i = 0; i < inputs.length; i++) {
+ const table = lunr.tokenizer.table
+ const total = table.length
+
+ /* Split string into sections and tokenize content blocks */
+ extract(inputs[i], (block, type, start, end) => {
+ table[block += total] ||= []
+ switch (type) {
+
+ /* Handle markup */
+ case Extract.TAG_OPEN:
+ case Extract.TAG_CLOSE:
+ table[block].push(
+ start << 12 |
+ end - start << 2 |
+ type
+ )
+ break
+
+ /* Handle text content */
+ case Extract.TEXT:
+ const section = inputs[i].slice(start, end)
+ split(section, lunr.tokenizer.separator, (index, until) => {
+
+ /**
+ * Apply segmenter after tokenization. Note that the segmenter will
+ * also split words at word boundaries, which is not what we want,
+ * so we need to check if we can somehow mitigate this behavior.
+ */
+ if (typeof lunr.segmenter !== "undefined") {
+ const subsection = section.slice(index, until)
+ if (/^[MHIK]$/.test(lunr.segmenter.ctype_(subsection))) {
+ const segments = lunr.segmenter.segment(subsection)
+ for (let s = 0, l = 0; s < segments.length; s++) {
+
+ /* Add block to section */
+ table[block] ||= []
+ table[block].push(
+ start + index + l << 12 |
+ segments[s].length << 2 |
+ type
+ )
+
+ /* Add token with position */
+ tokens.push(new lunr.Token(
+ segments[s].toLowerCase(), {
+ position: block << 20 | table[block].length - 1
+ }
+ ))
+
+ /* Keep track of length */
+ l += segments[s].length
+ }
+ return
+ }
+ }
+
+ /* Add block to section */
+ table[block].push(
+ start + index << 12 |
+ until - index << 2 |
+ type
+ )
+
+ /* Add token with position */
+ tokens.push(new lunr.Token(
+ section.slice(index, until).toLowerCase(), {
+ position: block << 20 | table[block].length - 1
+ }
+ ))
+ })
+ }
+ })
+ }
+
+ /* Return tokens */
+ return tokens
+}
diff --git a/src/templates/assets/javascripts/integrations/search/query/.eslintrc b/src/templates/assets/javascripts/integrations/search/query/.eslintrc
new file mode 100644
index 00000000..3031c7e3
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/query/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "rules": {
+ "no-control-regex": "off",
+ "@typescript-eslint/no-explicit-any": "off"
+ }
+}
diff --git a/src/templates/assets/javascripts/integrations/search/query/_/index.ts b/src/templates/assets/javascripts/integrations/search/query/_/index.ts
new file mode 100644
index 00000000..14482e43
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/query/_/index.ts
@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { split } from "../../internal"
+import { transform } from "../transform"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search query clause
+ */
+export interface SearchQueryClause {
+ presence: lunr.Query.presence /* Clause presence */
+ term: string /* Clause term */
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Search query terms
+ */
+export type SearchQueryTerms = Record<string, boolean>
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Transform search query
+ *
+ * This function lexes the given search query and applies the transformation
+ * function to each term, preserving markup like `+` and `-` modifiers.
+ *
+ * @param query - Search query
+ *
+ * @returns Search query
+ */
+export function transformSearchQuery(
+ query: string
+): string {
+
+ /* Split query terms with tokenizer */
+ return transform(query, part => {
+ const terms: string[] = []
+
+ /* Initialize lexer and analyze part */
+ const lexer = new lunr.QueryLexer(part)
+ lexer.run()
+
+ /* Extract and tokenize term from lexeme */
+ for (const { type, str: term, start, end } of lexer.lexemes)
+ switch (type) {
+
+ /* Hack: remove colon - see https://bit.ly/3wD3T3I */
+ case "FIELD":
+ if (!["title", "text", "tags"].includes(term))
+ part = [
+ part.slice(0, end),
+ " ",
+ part.slice(end + 1)
+ ].join("")
+ break
+
+ /* Tokenize term */
+ case "TERM":
+ split(term, lunr.tokenizer.separator, (...range) => {
+ terms.push([
+ part.slice(0, start),
+ term.slice(...range),
+ part.slice(end)
+ ].join(""))
+ })
+ }
+
+ /* Return terms */
+ return terms
+ })
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Parse a search query for analysis
+ *
+ * Lunr.js itself has a bug where it doesn't detect or remove wildcards for
+ * query clauses, so we must do this here.
+ *
+ * @see https://bit.ly/3DpTGtz - GitHub issue
+ *
+ * @param value - Query value
+ *
+ * @returns Search query clauses
+ */
+export function parseSearchQuery(
+ value: string
+): SearchQueryClause[] {
+ const query = new lunr.Query(["title", "text", "tags"])
+ const parser = new lunr.QueryParser(value, query)
+
+ /* Parse Search query */
+ parser.parse()
+ for (const clause of query.clauses) {
+ clause.usePipeline = true
+
+ /* Handle leading wildcard */
+ if (clause.term.startsWith("*")) {
+ clause.wildcard = lunr.Query.wildcard.LEADING
+ clause.term = clause.term.slice(1)
+ }
+
+ /* Handle trailing wildcard */
+ if (clause.term.endsWith("*")) {
+ clause.wildcard = lunr.Query.wildcard.TRAILING
+ clause.term = clause.term.slice(0, -1)
+ }
+ }
+
+ /* Return query clauses */
+ return query.clauses
+}
+
+/**
+ * Analyze the search query clauses in regard to the search terms found
+ *
+ * @param query - Search query clauses
+ * @param terms - Search terms
+ *
+ * @returns Search query terms
+ */
+export function getSearchQueryTerms(
+ query: SearchQueryClause[], terms: string[]
+): SearchQueryTerms {
+ const clauses = new Set<SearchQueryClause>(query)
+
+ /* Match query clauses against terms */
+ const result: SearchQueryTerms = {}
+ for (let t = 0; t < terms.length; t++)
+ for (const clause of clauses)
+ if (terms[t].startsWith(clause.term)) {
+ result[clause.term] = true
+ clauses.delete(clause)
+ }
+
+ /* Annotate unmatched non-stopword query clauses */
+ for (const clause of clauses)
+ if (lunr.stopWordFilter?.(clause.term))
+ result[clause.term] = false
+
+ /* Return query terms */
+ return result
+}
diff --git a/src/templates/assets/javascripts/integrations/search/query/index.ts b/src/templates/assets/javascripts/integrations/search/query/index.ts
new file mode 100644
index 00000000..763e2fd4
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/query/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./segment"
+export * from "./transform"
diff --git a/src/templates/assets/javascripts/integrations/search/query/segment/index.ts b/src/templates/assets/javascripts/integrations/search/query/segment/index.ts
new file mode 100644
index 00000000..b96796f4
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/query/segment/index.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Segment a search query using the inverted index
+ *
+ * This function implements a clever approach to text segmentation for Asian
+ * languages, as it used the information already available in the search index.
+ * The idea is to greedily segment the search query based on the tokens that are
+ * already part of the index, as described in the linked issue.
+ *
+ * @see https://bit.ly/3lwjrk7 - GitHub issue
+ *
+ * @param query - Query value
+ * @param index - Inverted index
+ *
+ * @returns Segmented query value
+ */
+export function segment(
+ query: string, index: object
+): Iterable<string> {
+ const segments = new Set<string>()
+
+ /* Segment search query */
+ const wordcuts = new Uint16Array(query.length)
+ for (let i = 0; i < query.length; i++)
+ for (let j = i + 1; j < query.length; j++) {
+ const value = query.slice(i, j)
+ if (value in index)
+ wordcuts[i] = j - i
+ }
+
+ /* Compute longest matches with minimum overlap */
+ const stack = [0]
+ for (let s = stack.length; s > 0;) {
+ const p = stack[--s]
+ for (let q = 1; q < wordcuts[p]; q++)
+ if (wordcuts[p + q] > wordcuts[p] - q) {
+ segments.add(query.slice(p, p + q))
+ stack[s++] = p + q
+ }
+
+ /* Continue at end of query string */
+ const q = p + wordcuts[p]
+ if (wordcuts[q] && q < query.length - 1)
+ stack[s++] = q
+
+ /* Add current segment */
+ segments.add(query.slice(p, q))
+ }
+
+ // @todo fix this case in the code block above, this is a hotfix
+ if (segments.has(""))
+ return new Set([query])
+
+ /* Return segmented query value */
+ return segments
+}
diff --git a/src/templates/assets/javascripts/integrations/search/query/transform/index.ts b/src/templates/assets/javascripts/integrations/search/query/transform/index.ts
new file mode 100644
index 00000000..41497786
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/query/transform/index.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Visitor function
+ *
+ * @param value - String value
+ *
+ * @returns String term(s)
+ */
+type VisitorFn = (
+ value: string
+) => string | string[]
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Default transformation function
+ *
+ * 1. Trim excess whitespace from left and right.
+ *
+ * 2. Search for parts in quotation marks and prepend a `+` modifier to denote
+ * that the resulting document must contain all parts, converting the query
+ * to an `AND` query (as opposed to the default `OR` behavior). While users
+ * may expect parts enclosed in quotation marks to map to span queries, i.e.
+ * for which order is important, Lunr.js doesn't support them, so the best
+ * we can do is to convert the parts to an `AND` query.
+ *
+ * 3. Replace control characters which are not located at the beginning of the
+ * query or preceded by white space, or are not followed by a non-whitespace
+ * character or are at the end of the query string. Furthermore, filter
+ * unmatched quotation marks.
+ *
+ * 4. Split the query string at whitespace, then pass each part to the visitor
+ * function for tokenization, and append a wildcard to every resulting term
+ * that is not explicitly marked with a `+`, `-`, `~` or `^` modifier, since
+ * it ensures consistent and stable ranking when multiple terms are entered.
+ * Also, if a fuzzy or boost modifier are given, but no numeric value has
+ * been entered, default to 1 to not induce a query error.
+ *
+ * @param query - Query value
+ * @param fn - Visitor function
+ *
+ * @returns Transformed query value
+ */
+export function transform(
+ query: string, fn: VisitorFn = term => term
+): string {
+ return query
+
+ /* => 1 */
+ .trim()
+
+ /* => 2 */
+ .split(/"([^"]+)"/g)
+ .map((parts, index) => index & 1
+ ? parts.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +")
+ : parts
+ )
+ .join("")
+
+ /* => 3 */
+ .replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "")
+
+ /* => 4 */
+ .split(/\s+/g)
+ .reduce((prev, term) => {
+ const next = fn(term)
+ return [...prev, ...Array.isArray(next) ? next : [next]]
+ }, [] as string[])
+ .map(term => /([~^]$)/.test(term) ? `${term}1` : term)
+ .map(term => /(^[+-]|[~^]\d+$)/.test(term) ? term : `${term}*`)
+ .join(" ")
+}
diff --git a/src/templates/assets/javascripts/integrations/search/worker/_/index.ts b/src/templates/assets/javascripts/integrations/search/worker/_/index.ts
new file mode 100644
index 00000000..26713573
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/worker/_/index.ts
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ ObservableInput,
+ Subject,
+ first,
+ merge,
+ of,
+ switchMap
+} from "rxjs"
+
+import { feature } from "~/_"
+import { watchToggle, watchWorker } from "~/browser"
+
+import { SearchIndex } from "../../config"
+import {
+ SearchMessage,
+ SearchMessageType
+} from "../message"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Set up search worker
+ *
+ * This function creates and initializes a web worker that is used for search,
+ * so that the user interface doesn't freeze. In general, the application does
+ * not care how search is implemented, as long as the web worker conforms to
+ * the format expected by the application as defined in `SearchMessage`. This
+ * allows the author to implement custom search functionality, by providing a
+ * custom web worker via configuration.
+ *
+ * Material for MkDocs' built-in search implementation makes use of Lunr.js, an
+ * efficient and fast implementation for client-side search. Leveraging a tiny
+ * iframe-based web worker shim, search is even supported for the `file://`
+ * protocol, enabling search for local non-hosted builds.
+ *
+ * If the protocol is `file://`, search initialization is deferred to mitigate
+ * freezing, as it's now synchronous by design - see https://bit.ly/3C521EO
+ *
+ * @see https://bit.ly/3igvtQv - How to implement custom search
+ *
+ * @param url - Worker URL
+ * @param index$ - Search index observable input
+ *
+ * @returns Search worker
+ */
+export function setupSearchWorker(
+ url: string, index$: ObservableInput<SearchIndex>
+): Subject<SearchMessage> {
+ const worker$ = watchWorker<SearchMessage>(url)
+ merge(
+ of(location.protocol !== "file:"),
+ watchToggle("search")
+ )
+ .pipe(
+ first(active => active),
+ switchMap(() => index$)
+ )
+ .subscribe(({ config, docs }) => worker$.next({
+ type: SearchMessageType.SETUP,
+ data: {
+ config,
+ docs,
+ options: {
+ suggest: feature("search.suggest")
+ }
+ }
+ }))
+
+ /* Return search worker */
+ return worker$
+}
diff --git a/src/templates/assets/javascripts/integrations/search/worker/index.ts b/src/templates/assets/javascripts/integrations/search/worker/index.ts
new file mode 100644
index 00000000..7120ad6e
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/worker/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./_"
+export * from "./message"
diff --git a/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc b/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc
new file mode 100644
index 00000000..3df9d551
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "rules": {
+ "no-console": "off",
+ "@typescript-eslint/no-misused-promises": "off"
+ }
+}
diff --git a/src/templates/assets/javascripts/integrations/search/worker/main/index.ts b/src/templates/assets/javascripts/integrations/search/worker/main/index.ts
new file mode 100644
index 00000000..2df38080
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/worker/main/index.ts
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import lunr from "lunr"
+
+import { getElement } from "~/browser/element/_"
+import "~/polyfills"
+
+import { Search } from "../../_"
+import { SearchConfig } from "../../config"
+import {
+ SearchMessage,
+ SearchMessageType
+} from "../message"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Add support for `iframe-worker` shim
+ *
+ * While `importScripts` is synchronous when executed inside of a web worker,
+ * it's not possible to provide a synchronous shim implementation. The cool
+ * thing is that awaiting a non-Promise will convert it into a Promise, so
+ * extending the type definition to return a `Promise` shouldn't break anything.
+ *
+ * @see https://bit.ly/2PjDnXi - GitHub comment
+ *
+ * @param urls - Scripts to load
+ *
+ * @returns Promise resolving with no result
+ */
+declare global {
+ function importScripts(...urls: string[]): Promise<void> | void
+}
+
+/* ----------------------------------------------------------------------------
+ * Data
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search index
+ */
+let index: Search
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Fetch (= import) multi-language support through `lunr-languages`
+ *
+ * This function automatically imports the stemmers necessary to process the
+ * languages which are defined as part of the search configuration.
+ *
+ * If the worker runs inside of an `iframe` (when using `iframe-worker` as
+ * a shim), the base URL for the stemmers to be loaded must be determined by
+ * searching for the first `script` element with a `src` attribute, which will
+ * contain the contents of this script.
+ *
+ * @param config - Search configuration
+ *
+ * @returns Promise resolving with no result
+ */
+async function setupSearchLanguages(
+ config: SearchConfig
+): Promise<void> {
+ let base = "../lunr"
+
+ /* Detect `iframe-worker` and fix base URL */
+ if (typeof parent !== "undefined" && "IFrameWorker" in parent) {
+ const worker = getElement<HTMLScriptElement>("script[src]")!
+ const [path] = worker.src.split("/worker")
+
+ /* Prefix base with path */
+ base = base.replace("..", path)
+ }
+
+ /* Add scripts for languages */
+ const scripts = []
+ for (const lang of config.lang) {
+ switch (lang) {
+
+ /* Add segmenter for Japanese */
+ case "ja":
+ scripts.push(`${base}/tinyseg.js`)
+ break
+
+ /* Add segmenter for Hindi and Thai */
+ case "hi":
+ case "th":
+ scripts.push(`${base}/wordcut.js`)
+ break
+ }
+
+ /* Add language support */
+ if (lang !== "en")
+ scripts.push(`${base}/min/lunr.${lang}.min.js`)
+ }
+
+ /* Add multi-language support */
+ if (config.lang.length > 1)
+ scripts.push(`${base}/min/lunr.multi.min.js`)
+
+ /* Load scripts synchronously */
+ if (scripts.length)
+ await importScripts(
+ `${base}/min/lunr.stemmer.support.min.js`,
+ ...scripts
+ )
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Message handler
+ *
+ * @param message - Source message
+ *
+ * @returns Target message
+ */
+export async function handler(
+ message: SearchMessage
+): Promise<SearchMessage> {
+ switch (message.type) {
+
+ /* Search setup message */
+ case SearchMessageType.SETUP:
+ await setupSearchLanguages(message.data.config)
+ index = new Search(message.data)
+ return {
+ type: SearchMessageType.READY
+ }
+
+ /* Search query message */
+ case SearchMessageType.QUERY:
+ const query = message.data
+ try {
+ return {
+ type: SearchMessageType.RESULT,
+ data: index.search(query)
+ }
+
+ /* Return empty result in case of error */
+ } catch (err) {
+ console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`)
+ console.warn(err)
+ return {
+ type: SearchMessageType.RESULT,
+ data: { items: [] }
+ }
+ }
+
+ /* All other messages */
+ default:
+ throw new TypeError("Invalid message type")
+ }
+}
+
+/* ----------------------------------------------------------------------------
+ * Worker
+ * ------------------------------------------------------------------------- */
+
+/* Expose Lunr.js in global scope, or stemmers won't work */
+self.lunr = lunr
+
+/* Handle messages */
+addEventListener("message", async ev => {
+ postMessage(await handler(ev.data))
+})
diff --git a/src/templates/assets/javascripts/integrations/search/worker/message/index.ts b/src/templates/assets/javascripts/integrations/search/worker/message/index.ts
new file mode 100644
index 00000000..54d5001e
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/search/worker/message/index.ts
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { SearchResult } from "../../_"
+import { SearchIndex } from "../../config"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Search message type
+ */
+export const enum SearchMessageType {
+ SETUP, /* Search index setup */
+ READY, /* Search index ready */
+ QUERY, /* Search query */
+ RESULT /* Search results */
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Message containing the data necessary to setup the search index
+ */
+export interface SearchSetupMessage {
+ type: SearchMessageType.SETUP /* Message type */
+ data: SearchIndex /* Message data */
+}
+
+/**
+ * Message indicating the search index is ready
+ */
+export interface SearchReadyMessage {
+ type: SearchMessageType.READY /* Message type */
+}
+
+/**
+ * Message containing a search query
+ */
+export interface SearchQueryMessage {
+ type: SearchMessageType.QUERY /* Message type */
+ data: string /* Message data */
+}
+
+/**
+ * Message containing results for a search query
+ */
+export interface SearchResultMessage {
+ type: SearchMessageType.RESULT /* Message type */
+ data: SearchResult /* Message data */
+}
+
+/* ------------------------------------------------------------------------- */
+
+/**
+ * Message exchanged with the search worker
+ */
+export type SearchMessage =
+ | SearchSetupMessage
+ | SearchReadyMessage
+ | SearchQueryMessage
+ | SearchResultMessage
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Type guard for search ready messages
+ *
+ * @param message - Search worker message
+ *
+ * @returns Test result
+ */
+export function isSearchReadyMessage(
+ message: SearchMessage
+): message is SearchReadyMessage {
+ return message.type === SearchMessageType.READY
+}
+
+/**
+ * Type guard for search result messages
+ *
+ * @param message - Search worker message
+ *
+ * @returns Test result
+ */
+export function isSearchResultMessage(
+ message: SearchMessage
+): message is SearchResultMessage {
+ return message.type === SearchMessageType.RESULT
+}
diff --git a/src/templates/assets/javascripts/integrations/sitemap/index.ts b/src/templates/assets/javascripts/integrations/sitemap/index.ts
new file mode 100644
index 00000000..08695bad
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/sitemap/index.ts
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Observable,
+ catchError,
+ defaultIfEmpty,
+ map,
+ of,
+ tap
+} from "rxjs"
+
+import { configuration } from "~/_"
+import { getElements, requestXML } from "~/browser"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Sitemap, i.e. a list of URLs
+ */
+export type Sitemap = string[]
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Preprocess a list of URLs
+ *
+ * This function replaces the `site_url` in the sitemap with the actual base
+ * URL, to allow instant navigation to work in occasions like Netlify previews.
+ *
+ * @param urls - URLs
+ *
+ * @returns URL path parts
+ */
+function preprocess(urls: Sitemap): Sitemap {
+ if (urls.length < 2)
+ return [""]
+
+ /* Take the first two URLs and remove everything after the last slash */
+ const [root, next] = [...urls]
+ .sort((a, b) => a.length - b.length)
+ .map(url => url.replace(/[^/]+$/, ""))
+
+ /* Compute common prefix */
+ let index = 0
+ if (root === next)
+ index = root.length
+ else
+ while (root.charCodeAt(index) === next.charCodeAt(index))
+ index++
+
+ /* Remove common prefix and return in original order */
+ return urls.map(url => url.replace(root.slice(0, index), ""))
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Fetch the sitemap for the given base URL
+ *
+ * @param base - Base URL
+ *
+ * @returns Sitemap observable
+ */
+export function fetchSitemap(base?: URL): Observable<Sitemap> {
+ const cached = __md_get<Sitemap>("__sitemap", sessionStorage, base)
+ if (cached) {
+ return of(cached)
+ } else {
+ const config = configuration()
+ return requestXML(new URL("sitemap.xml", base || config.base))
+ .pipe(
+ map(sitemap => preprocess(getElements("loc", sitemap)
+ .map(node => node.textContent!)
+ )),
+ catchError(() => EMPTY), // @todo refactor instant loading
+ defaultIfEmpty([]),
+ tap(sitemap => __md_set("__sitemap", sitemap, sessionStorage, base))
+ )
+ }
+}
diff --git a/src/templates/assets/javascripts/integrations/version/.eslintrc b/src/templates/assets/javascripts/integrations/version/.eslintrc
new file mode 100644
index 00000000..38a5714d
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/version/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "rules": {
+ "no-null/no-null": "off"
+ }
+}
diff --git a/src/templates/assets/javascripts/integrations/version/index.ts b/src/templates/assets/javascripts/integrations/version/index.ts
new file mode 100644
index 00000000..38d78f17
--- /dev/null
+++ b/src/templates/assets/javascripts/integrations/version/index.ts
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ EMPTY,
+ Subject,
+ catchError,
+ combineLatest,
+ filter,
+ fromEvent,
+ map,
+ of,
+ switchMap,
+ withLatestFrom
+} from "rxjs"
+
+import { configuration } from "~/_"
+import {
+ getElement,
+ getLocation,
+ requestJSON,
+ setLocation
+} from "~/browser"
+import { getComponentElements } from "~/components"
+import {
+ Version,
+ renderVersionSelector
+} from "~/templates"
+
+import { fetchSitemap } from "../sitemap"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Setup options
+ */
+interface SetupOptions {
+ document$: Subject<Document> /* Document subject */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Set up version selector
+ *
+ * @param options - Options
+ */
+export function setupVersionSelector(
+ { document$ }: SetupOptions
+): void {
+ const config = configuration()
+ const versions$ = requestJSON<Version[]>(
+ new URL("../versions.json", config.base)
+ )
+ .pipe(
+ catchError(() => EMPTY) // @todo refactor instant loading
+ )
+
+ /* Determine current version */
+ const current$ = versions$
+ .pipe(
+ map(versions => {
+ const [, current] = config.base.match(/([^/]+)\/?$/)!
+ return versions.find(({ version, aliases }) => (
+ version === current || aliases.includes(current)
+ )) || versions[0]
+ })
+ )
+
+ /* Intercept inter-version navigation */
+ versions$
+ .pipe(
+ map(versions => new Map(versions.map(version => [
+ `${new URL(`../${version.version}/`, config.base)}`,
+ version
+ ]))),
+ switchMap(urls => fromEvent<MouseEvent>(document.body, "click")
+ .pipe(
+ filter(ev => !ev.metaKey && !ev.ctrlKey),
+ withLatestFrom(current$),
+ switchMap(([ev, current]) => {
+ if (ev.target instanceof Element) {
+ const el = ev.target.closest("a")
+ if (el && !el.target && urls.has(el.href)) {
+ const url = el.href
+ // This is a temporary hack to detect if a version inside the
+ // version selector or on another part of the site was clicked.
+ // If we're inside the version selector, we definitely want to
+ // find the same page, as we might have different deployments
+ // due to aliases. However, if we're outside the version
+ // selector, we must abort here, because we might otherwise
+ // interfere with instant navigation. We need to refactor this
+ // at some point together with instant navigation.
+ //
+ // See https://github.com/squidfunk/mkdocs-material/issues/4012
+ if (!ev.target.closest(".md-version")) {
+ const version = urls.get(url)!
+ if (version === current)
+ return EMPTY
+ }
+ ev.preventDefault()
+ return of(url)
+ }
+ }
+ return EMPTY
+ }),
+ switchMap(url => {
+ const { version } = urls.get(url)!
+ return fetchSitemap(new URL(url))
+ .pipe(
+ map(sitemap => {
+ const location = getLocation()
+ const path = location.href.replace(config.base, "")
+ return sitemap.includes(path.split("#")[0])
+ ? new URL(`../${version}/${path}`, config.base)
+ : new URL(url)
+ })
+ )
+ })
+ )
+ )
+ )
+ .subscribe(url => setLocation(url, true))
+
+ /* Render version selector and warning */
+ combineLatest([versions$, current$])
+ .subscribe(([versions, current]) => {
+ const topic = getElement(".md-header__topic")
+ topic.appendChild(renderVersionSelector(versions, current))
+ })
+
+ /* Integrate outdated version banner with instant navigation */
+ document$.pipe(switchMap(() => current$))
+ .subscribe(current => {
+
+ /* Check if version state was already determined */
+ let outdated = __md_get("__outdated", sessionStorage)
+ if (outdated === null) {
+ outdated = true
+
+ /* Obtain and normalize default versions */
+ let ignored = config.version?.default || "latest"
+ if (!Array.isArray(ignored))
+ ignored = [ignored]
+
+ /* Check if version is considered a default */
+ main: for (const ignore of ignored)
+ for (const alias of current.aliases)
+ if (new RegExp(ignore, "i").test(alias)) {
+ outdated = false
+ break main
+ }
+
+ /* Persist version state in session storage */
+ __md_set("__outdated", outdated, sessionStorage)
+ }
+
+ /* Unhide outdated version banner */
+ if (outdated)
+ for (const warning of getComponentElements("outdated"))
+ warning.hidden = false
+ })
+}
diff --git a/src/templates/assets/javascripts/patches/indeterminate/index.ts b/src/templates/assets/javascripts/patches/indeterminate/index.ts
new file mode 100644
index 00000000..9b7b0d5a
--- /dev/null
+++ b/src/templates/assets/javascripts/patches/indeterminate/index.ts
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ fromEvent,
+ map,
+ mergeMap,
+ switchMap,
+ takeWhile,
+ tap,
+ withLatestFrom
+} from "rxjs"
+
+import { getElements } from "~/browser"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Patch options
+ */
+interface PatchOptions {
+ document$: Observable<Document> /* Document observable */
+ tablet$: Observable<boolean> /* Media tablet observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Patch indeterminate checkboxes
+ *
+ * This function replaces the indeterminate "pseudo state" with the actual
+ * indeterminate state, which is used to keep navigation always expanded.
+ *
+ * @param options - Options
+ */
+export function patchIndeterminate(
+ { document$, tablet$ }: PatchOptions
+): void {
+ document$
+ .pipe(
+ switchMap(() => getElements<HTMLInputElement>(
+ ".md-toggle--indeterminate"
+ )),
+ tap(el => {
+ el.indeterminate = true
+ el.checked = false
+ }),
+ mergeMap(el => fromEvent(el, "change")
+ .pipe(
+ takeWhile(() => el.classList.contains("md-toggle--indeterminate")),
+ map(() => el)
+ )
+ ),
+ withLatestFrom(tablet$)
+ )
+ .subscribe(([el, tablet]) => {
+ el.classList.remove("md-toggle--indeterminate")
+ if (tablet)
+ el.checked = false
+ })
+}
diff --git a/src/templates/assets/javascripts/patches/index.ts b/src/templates/assets/javascripts/patches/index.ts
new file mode 100644
index 00000000..b6e65fc0
--- /dev/null
+++ b/src/templates/assets/javascripts/patches/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./indeterminate"
+export * from "./scrollfix"
+export * from "./scrolllock"
diff --git a/src/templates/assets/javascripts/patches/scrollfix/index.ts b/src/templates/assets/javascripts/patches/scrollfix/index.ts
new file mode 100644
index 00000000..607c46a0
--- /dev/null
+++ b/src/templates/assets/javascripts/patches/scrollfix/index.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ filter,
+ fromEvent,
+ map,
+ mergeMap,
+ switchMap,
+ tap
+} from "rxjs"
+
+import { getElements } from "~/browser"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Patch options
+ */
+interface PatchOptions {
+ document$: Observable<Document> /* Document observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Check whether the given device is an Apple device
+ *
+ * @returns Test result
+ */
+function isAppleDevice(): boolean {
+ return /(iPad|iPhone|iPod)/.test(navigator.userAgent)
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Patch all elements with `data-md-scrollfix` attributes
+ *
+ * This is a year-old patch which ensures that overflow scrolling works at the
+ * top and bottom of containers on iOS by ensuring a `1px` scroll offset upon
+ * the start of a touch event.
+ *
+ * @see https://bit.ly/2SCtAOO - Original source
+ *
+ * @param options - Options
+ */
+export function patchScrollfix(
+ { document$ }: PatchOptions
+): void {
+ document$
+ .pipe(
+ switchMap(() => getElements("[data-md-scrollfix]")),
+ tap(el => el.removeAttribute("data-md-scrollfix")),
+ filter(isAppleDevice),
+ mergeMap(el => fromEvent(el, "touchstart")
+ .pipe(
+ map(() => el)
+ )
+ )
+ )
+ .subscribe(el => {
+ const top = el.scrollTop
+
+ /* We're at the top of the container */
+ if (top === 0) {
+ el.scrollTop = 1
+
+ /* We're at the bottom of the container */
+ } else if (top + el.offsetHeight === el.scrollHeight) {
+ el.scrollTop = top - 1
+ }
+ })
+}
diff --git a/src/templates/assets/javascripts/patches/scrolllock/index.ts b/src/templates/assets/javascripts/patches/scrolllock/index.ts
new file mode 100644
index 00000000..4ec3e103
--- /dev/null
+++ b/src/templates/assets/javascripts/patches/scrolllock/index.ts
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import {
+ Observable,
+ combineLatest,
+ delay,
+ map,
+ of,
+ switchMap,
+ withLatestFrom
+} from "rxjs"
+
+import {
+ Viewport,
+ watchToggle
+} from "~/browser"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Patch options
+ */
+interface PatchOptions {
+ viewport$: Observable<Viewport> /* Viewport observable */
+ tablet$: Observable<boolean> /* Media tablet observable */
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Patch the document body to lock when search is open
+ *
+ * For mobile and tablet viewports, the search is rendered full screen, which
+ * leads to scroll leaking when at the top or bottom of the search result. This
+ * function locks the body when the search is in full screen mode, and restores
+ * the scroll position when leaving.
+ *
+ * @param options - Options
+ */
+export function patchScrolllock(
+ { viewport$, tablet$ }: PatchOptions
+): void {
+ combineLatest([watchToggle("search"), tablet$])
+ .pipe(
+ map(([active, tablet]) => active && !tablet),
+ switchMap(active => of(active)
+ .pipe(
+ delay(active ? 400 : 100)
+ )
+ ),
+ withLatestFrom(viewport$)
+ )
+ .subscribe(([active, { offset: { y }}]) => {
+ if (active) {
+ document.body.setAttribute("data-md-scrolllock", "")
+ document.body.style.top = `-${y}px`
+ } else {
+ const value = -1 * parseInt(document.body.style.top, 10)
+ document.body.removeAttribute("data-md-scrolllock")
+ document.body.style.top = ""
+ if (value)
+ window.scrollTo(0, value)
+ }
+ })
+}
diff --git a/src/templates/assets/javascripts/polyfills/index.ts b/src/templates/assets/javascripts/polyfills/index.ts
new file mode 100644
index 00000000..2aec8290
--- /dev/null
+++ b/src/templates/assets/javascripts/polyfills/index.ts
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Polyfills
+ * ------------------------------------------------------------------------- */
+
+/* Polyfill `Object.entries` */
+if (!Object.entries)
+ Object.entries = function (obj: object) {
+ const data: [string, string][] = []
+ for (const key of Object.keys(obj))
+ // @ts-expect-error - ignore property access warning
+ data.push([key, obj[key]])
+
+ /* Return entries */
+ return data
+ }
+
+/* Polyfill `Object.values` */
+if (!Object.values)
+ Object.values = function (obj: object) {
+ const data: string[] = []
+ for (const key of Object.keys(obj))
+ // @ts-expect-error - ignore property access warning
+ data.push(obj[key])
+
+ /* Return values */
+ return data
+ }
+
+/* ------------------------------------------------------------------------- */
+
+/* Polyfills for `Element` */
+if (typeof Element !== "undefined") {
+
+ /* Polyfill `Element.scrollTo` */
+ if (!Element.prototype.scrollTo)
+ Element.prototype.scrollTo = function (
+ x?: ScrollToOptions | number, y?: number
+ ): void {
+ if (typeof x === "object") {
+ this.scrollLeft = x.left!
+ this.scrollTop = x.top!
+ } else {
+ this.scrollLeft = x!
+ this.scrollTop = y!
+ }
+ }
+
+ /* Polyfill `Element.replaceWith` */
+ if (!Element.prototype.replaceWith)
+ Element.prototype.replaceWith = function (
+ ...nodes: Array<string | Node>
+ ): void {
+ const parent = this.parentNode
+ if (parent) {
+ if (nodes.length === 0)
+ parent.removeChild(this)
+
+ /* Replace children and create text nodes */
+ for (let i = nodes.length - 1; i >= 0; i--) {
+ let node = nodes[i]
+ if (typeof node === "string")
+ node = document.createTextNode(node)
+ else if (node.parentNode)
+ node.parentNode.removeChild(node)
+
+ /* Replace child or insert before previous sibling */
+ if (!i)
+ parent.replaceChild(node, this)
+ else
+ parent.insertBefore(this.previousSibling!, node)
+ }
+ }
+ }
+}
diff --git a/src/templates/assets/javascripts/templates/annotation/index.tsx b/src/templates/assets/javascripts/templates/annotation/index.tsx
new file mode 100644
index 00000000..9b8f85f5
--- /dev/null
+++ b/src/templates/assets/javascripts/templates/annotation/index.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { h } from "~/utilities"
+
+import { renderTooltip } from "../tooltip"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render an annotation
+ *
+ * @param id - Annotation identifier
+ * @param prefix - Tooltip identifier prefix
+ *
+ * @returns Element
+ */
+export function renderAnnotation(
+ id: string | number, prefix?: string
+): HTMLElement {
+ prefix = prefix ? `${prefix}_annotation_${id}` : undefined
+
+ /* Render tooltip with anchor, if given */
+ if (prefix) {
+ const anchor = prefix ? `#${prefix}` : undefined
+ return (
+ <aside class="md-annotation" tabIndex={0}>
+ {renderTooltip(prefix)}
+ <a href={anchor} class="md-annotation__index" tabIndex={-1}>
+ <span data-md-annotation-id={id}></span>
+ </a>
+ </aside>
+ )
+ } else {
+ return (
+ <aside class="md-annotation" tabIndex={0}>
+ {renderTooltip(prefix)}
+ <span class="md-annotation__index" tabIndex={-1}>
+ <span data-md-annotation-id={id}></span>
+ </span>
+ </aside>
+ )
+ }
+}
diff --git a/src/templates/assets/javascripts/templates/clipboard/index.tsx b/src/templates/assets/javascripts/templates/clipboard/index.tsx
new file mode 100644
index 00000000..95dbf12a
--- /dev/null
+++ b/src/templates/assets/javascripts/templates/clipboard/index.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { translation } from "~/_"
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render a 'copy-to-clipboard' button
+ *
+ * @param id - Unique identifier
+ *
+ * @returns Element
+ */
+export function renderClipboardButton(id: string): HTMLElement {
+ return (
+ <button
+ class="md-clipboard md-icon"
+ title={translation("clipboard.copy")}
+ data-clipboard-target={`#${id} > code`}
+ ></button>
+ )
+}
diff --git a/src/templates/assets/javascripts/templates/index.ts b/src/templates/assets/javascripts/templates/index.ts
new file mode 100644
index 00000000..b50b93b8
--- /dev/null
+++ b/src/templates/assets/javascripts/templates/index.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./annotation"
+export * from "./clipboard"
+export * from "./search"
+export * from "./source"
+export * from "./tabbed"
+export * from "./table"
+export * from "./version"
diff --git a/src/templates/assets/javascripts/templates/search/index.tsx b/src/templates/assets/javascripts/templates/search/index.tsx
new file mode 100644
index 00000000..350c0505
--- /dev/null
+++ b/src/templates/assets/javascripts/templates/search/index.tsx
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { ComponentChild } from "preact"
+
+import { configuration, feature, translation } from "~/_"
+import { SearchItem } from "~/integrations/search"
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render flag
+ */
+const enum Flag {
+ TEASER = 1, /* Render teaser */
+ PARENT = 2 /* Render as parent */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper function
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render a search document
+ *
+ * @param document - Search document
+ * @param flag - Render flags
+ *
+ * @returns Element
+ */
+function renderSearchDocument(
+ document: SearchItem, flag: Flag
+): HTMLElement {
+ const parent = flag & Flag.PARENT
+ const teaser = flag & Flag.TEASER
+
+ /* Render missing query terms */
+ const missing = Object.keys(document.terms)
+ .filter(key => !document.terms[key])
+ .reduce<ComponentChild[]>((list, key) => [
+ ...list, <del>{key}</del>, " "
+ ], [])
+ .slice(0, -1)
+
+ /* Assemble query string for highlighting */
+ const config = configuration()
+ const url = new URL(document.location, config.base)
+ if (feature("search.highlight"))
+ url.searchParams.set("h", Object.entries(document.terms)
+ .filter(([, match]) => match)
+ .reduce((highlight, [value]) => `${highlight} ${value}`.trim(), "")
+ )
+
+ /* Render article or section, depending on flags */
+ const { tags } = configuration()
+ return (
+ <a href={`${url}`} class="md-search-result__link" tabIndex={-1}>
+ <article
+ class="md-search-result__article md-typeset"
+ data-md-score={document.score.toFixed(2)}
+ >
+ {parent > 0 && <div class="md-search-result__icon md-icon"></div>}
+ {parent > 0 && <h1>{document.title}</h1>}
+ {parent <= 0 && <h2>{document.title}</h2>}
+ {teaser > 0 && document.text.length > 0 &&
+ document.text
+ }
+ {document.tags && document.tags.map(tag => {
+ const type = tags
+ ? tag in tags
+ ? `md-tag-icon md-tag--${tags[tag]}`
+ : "md-tag-icon"
+ : ""
+ return (
+ <span class={`md-tag ${type}`}>{tag}</span>
+ )
+ })}
+ {teaser > 0 && missing.length > 0 &&
+ <p class="md-search-result__terms">
+ {translation("search.result.term.missing")}: {...missing}
+ </p>
+ }
+ </article>
+ </a>
+ )
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render a search result
+ *
+ * @param result - Search result
+ *
+ * @returns Element
+ */
+export function renderSearchResultItem(
+ result: SearchItem[]
+): HTMLElement {
+ const threshold = result[0].score
+ const docs = [...result]
+
+ const config = configuration()
+
+ /* Find and extract parent article */
+ const parent = docs.findIndex(doc => {
+ const l = `${new URL(doc.location, config.base)}` // @todo hacky
+ return !l.includes("#")
+ })
+ const [article] = docs.splice(parent, 1)
+
+ /* Determine last index above threshold */
+ let index = docs.findIndex(doc => doc.score < threshold)
+ if (index === -1)
+ index = docs.length
+
+ /* Partition sections */
+ const best = docs.slice(0, index)
+ const more = docs.slice(index)
+
+ /* Render children */
+ const children = [
+ renderSearchDocument(article, Flag.PARENT | +(!parent && index === 0)),
+ ...best.map(section => renderSearchDocument(section, Flag.TEASER)),
+ ...more.length ? [
+ <details class="md-search-result__more">
+ <summary tabIndex={-1}>
+ <div>
+ {more.length > 0 && more.length === 1
+ ? translation("search.result.more.one")
+ : translation("search.result.more.other", more.length)
+ }
+ </div>
+ </summary>
+ {...more.map(section => renderSearchDocument(section, Flag.TEASER))}
+ </details>
+ ] : []
+ ]
+
+ /* Render search result */
+ return (
+ <li class="md-search-result__item">
+ {children}
+ </li>
+ )
+}
diff --git a/src/templates/assets/javascripts/templates/source/index.tsx b/src/templates/assets/javascripts/templates/source/index.tsx
new file mode 100644
index 00000000..b59a8f67
--- /dev/null
+++ b/src/templates/assets/javascripts/templates/source/index.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { SourceFacts } from "~/components"
+import { h, round } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render repository facts
+ *
+ * @param facts - Repository facts
+ *
+ * @returns Element
+ */
+export function renderSourceFacts(facts: SourceFacts): HTMLElement {
+ return (
+ <ul class="md-source__facts">
+ {Object.entries(facts).map(([key, value]) => (
+ <li class={`md-source__fact md-source__fact--${key}`}>
+ {typeof value === "number" ? round(value) : value}
+ </li>
+ ))}
+ </ul>
+ )
+}
diff --git a/src/templates/assets/javascripts/templates/tabbed/index.tsx b/src/templates/assets/javascripts/templates/tabbed/index.tsx
new file mode 100644
index 00000000..b283ac66
--- /dev/null
+++ b/src/templates/assets/javascripts/templates/tabbed/index.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Tabbed control type
+ */
+type TabbedControlType =
+ | "prev"
+ | "next"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render control for content tabs
+ *
+ * @param type - Control type
+ *
+ * @returns Element
+ */
+export function renderTabbedControl(
+ type: TabbedControlType
+): HTMLElement {
+ const classes = `tabbed-control tabbed-control--${type}`
+ return (
+ <div class={classes} hidden>
+ <button class="tabbed-button" tabIndex={-1} aria-hidden="true"></button>
+ </div>
+ )
+}
diff --git a/src/templates/assets/javascripts/templates/table/index.tsx b/src/templates/assets/javascripts/templates/table/index.tsx
new file mode 100644
index 00000000..1fcba152
--- /dev/null
+++ b/src/templates/assets/javascripts/templates/table/index.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render a table inside a wrapper to improve scrolling on mobile
+ *
+ * @param table - Table element
+ *
+ * @returns Element
+ */
+export function renderTable(table: HTMLElement): HTMLElement {
+ return (
+ <div class="md-typeset__scrollwrap">
+ <div class="md-typeset__table">
+ {table}
+ </div>
+ </div>
+ )
+}
diff --git a/src/templates/assets/javascripts/templates/tooltip/index.tsx b/src/templates/assets/javascripts/templates/tooltip/index.tsx
new file mode 100644
index 00000000..ec583490
--- /dev/null
+++ b/src/templates/assets/javascripts/templates/tooltip/index.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render a tooltip
+ *
+ * @param id - Tooltip identifier
+ *
+ * @returns Element
+ */
+export function renderTooltip(id?: string): HTMLElement {
+ return (
+ <div class="md-tooltip" id={id}>
+ <div class="md-tooltip__inner md-typeset"></div>
+ </div>
+ )
+}
diff --git a/src/templates/assets/javascripts/templates/version/index.tsx b/src/templates/assets/javascripts/templates/version/index.tsx
new file mode 100644
index 00000000..4aff7aa7
--- /dev/null
+++ b/src/templates/assets/javascripts/templates/version/index.tsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { configuration, translation } from "~/_"
+import { h } from "~/utilities"
+
+/* ----------------------------------------------------------------------------
+ * Types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Version
+ */
+export interface Version {
+ version: string /* Version identifier */
+ title: string /* Version title */
+ aliases: string[] /* Version aliases */
+}
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render a version
+ *
+ * @param version - Version
+ *
+ * @returns Element
+ */
+function renderVersion(version: Version): HTMLElement {
+ const config = configuration()
+
+ /* Ensure trailing slash - see https://bit.ly/3rL5u3f */
+ const url = new URL(`../${version.version}/`, config.base)
+ return (
+ <li class="md-version__item">
+ <a href={`${url}`} class="md-version__link">
+ {version.title}
+ </a>
+ </li>
+ )
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Render a version selector
+ *
+ * @param versions - Versions
+ * @param active - Active version
+ *
+ * @returns Element
+ */
+export function renderVersionSelector(
+ versions: Version[], active: Version
+): HTMLElement {
+ return (
+ <div class="md-version">
+ <button
+ class="md-version__current"
+ aria-label={translation("select.version")}
+ >
+ {active.title}
+ </button>
+ <ul class="md-version__list">
+ {versions.map(renderVersion)}
+ </ul>
+ </div>
+ )
+}
diff --git a/src/templates/assets/javascripts/utilities/h/.eslintrc b/src/templates/assets/javascripts/utilities/h/.eslintrc
new file mode 100644
index 00000000..d79b45b0
--- /dev/null
+++ b/src/templates/assets/javascripts/utilities/h/.eslintrc
@@ -0,0 +1,7 @@
+{
+ "rules": {
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-namespace": "off",
+ "jsdoc/require-jsdoc": "off"
+ }
+}
diff --git a/src/templates/assets/javascripts/utilities/h/index.ts b/src/templates/assets/javascripts/utilities/h/index.ts
new file mode 100644
index 00000000..08d809f1
--- /dev/null
+++ b/src/templates/assets/javascripts/utilities/h/index.ts
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import { JSX as JSXInternal } from "preact"
+
+/* ----------------------------------------------------------------------------
+ * Helper types
+ * ------------------------------------------------------------------------- */
+
+/**
+ * HTML attributes
+ */
+type Attributes =
+ & JSXInternal.HTMLAttributes
+ & JSXInternal.SVGAttributes
+ & Record<string, any>
+
+/**
+ * Child element
+ */
+type Child =
+ | ChildNode
+ | HTMLElement
+ | Text
+ | string
+ | number
+
+/* ----------------------------------------------------------------------------
+ * Helper functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Append a child node to an element
+ *
+ * @param el - Element
+ * @param child - Child node(s)
+ */
+function appendChild(el: HTMLElement, child: Child | Child[]): void {
+
+ /* Handle primitive types (including raw HTML) */
+ if (typeof child === "string" || typeof child === "number") {
+ el.innerHTML += child.toString()
+
+ /* Handle nodes */
+ } else if (child instanceof Node) {
+ el.appendChild(child)
+
+ /* Handle nested children */
+ } else if (Array.isArray(child)) {
+ for (const node of child)
+ appendChild(el, node)
+ }
+}
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * JSX factory
+ *
+ * @template T - Element type
+ *
+ * @param tag - HTML tag
+ * @param attributes - HTML attributes
+ * @param children - Child elements
+ *
+ * @returns Element
+ */
+export function h<T extends keyof HTMLElementTagNameMap>(
+ tag: T, attributes?: Attributes | null, ...children: Child[]
+): HTMLElementTagNameMap[T]
+
+export function h<T extends h.JSX.Element>(
+ tag: string, attributes?: Attributes | null, ...children: Child[]
+): T
+
+export function h<T extends h.JSX.Element>(
+ tag: string, attributes?: Attributes | null, ...children: Child[]
+): T {
+ const el = document.createElement(tag)
+
+ /* Set attributes, if any */
+ if (attributes)
+ for (const attr of Object.keys(attributes)) {
+ if (typeof attributes[attr] === "undefined")
+ continue
+
+ /* Set default attribute or boolean */
+ if (typeof attributes[attr] !== "boolean")
+ el.setAttribute(attr, attributes[attr])
+ else
+ el.setAttribute(attr, "")
+ }
+
+ /* Append child nodes */
+ for (const child of children)
+ appendChild(el, child)
+
+ /* Return element */
+ return el as T
+}
+
+/* ----------------------------------------------------------------------------
+ * Namespace
+ * ------------------------------------------------------------------------- */
+
+export declare namespace h {
+ namespace JSX {
+ type Element = HTMLElement
+ type IntrinsicElements = JSXInternal.IntrinsicElements
+ }
+}
diff --git a/src/templates/assets/javascripts/utilities/index.ts b/src/templates/assets/javascripts/utilities/index.ts
new file mode 100644
index 00000000..42886e0b
--- /dev/null
+++ b/src/templates/assets/javascripts/utilities/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export * from "./h"
+export * from "./round"
diff --git a/src/templates/assets/javascripts/utilities/round/index.ts b/src/templates/assets/javascripts/utilities/round/index.ts
new file mode 100644
index 00000000..3e6bf91a
--- /dev/null
+++ b/src/templates/assets/javascripts/utilities/round/index.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* ----------------------------------------------------------------------------
+ * Functions
+ * ------------------------------------------------------------------------- */
+
+/**
+ * Round a number for display with repository facts
+ *
+ * This is a reverse-engineered version of GitHub's weird rounding algorithm
+ * for stars, forks and all other numbers. While all numbers below `1,000` are
+ * returned as-is, bigger numbers are converted to fixed numbers:
+ *
+ * - `1,049` => `1k`
+ * - `1,050` => `1.1k`
+ * - `1,949` => `1.9k`
+ * - `1,950` => `2k`
+ *
+ * @param value - Original value
+ *
+ * @returns Rounded value
+ */
+export function round(value: number): string {
+ if (value > 999) {
+ const digits = +((value - 950) % 1000 > 99)
+ return `${((value + 0.000001) / 1000).toFixed(digits)}k`
+ } else {
+ return value.toString()
+ }
+}
diff --git a/src/templates/assets/javascripts/workers/search.ts b/src/templates/assets/javascripts/workers/search.ts
new file mode 100644
index 00000000..e995b1ff
--- /dev/null
+++ b/src/templates/assets/javascripts/workers/search.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+import "~/integrations/search/worker/main"
diff --git a/src/templates/assets/stylesheets/_config.scss b/src/templates/assets/stylesheets/_config.scss
new file mode 100644
index 00000000..e64b8e29
--- /dev/null
+++ b/src/templates/assets/stylesheets/_config.scss
@@ -0,0 +1,42 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Variables: breakpoints
+// ----------------------------------------------------------------------------
+
+// Device-specific breakpoints
+$break-devices: (
+ mobile: (
+ portrait: px2em(220px) px2em(479.75px),
+ landscape: px2em(480px) px2em(719.75px)
+ ),
+ tablet: (
+ portrait: px2em(720px) px2em(959.75px),
+ landscape: px2em(960px) px2em(1219.75px)
+ ),
+ screen: (
+ small: px2em(1220px) px2em(1599.75px),
+ medium: px2em(1600px) px2em(1999.75px),
+ large: px2em(2000px)
+ )
+);
diff --git a/src/templates/assets/stylesheets/main.scss b/src/templates/assets/stylesheets/main.scss
new file mode 100644
index 00000000..2b203d3d
--- /dev/null
+++ b/src/templates/assets/stylesheets/main.scss
@@ -0,0 +1,86 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Dependencies
+// ----------------------------------------------------------------------------
+
+@import "material-color";
+@import "material-shadows";
+
+// ----------------------------------------------------------------------------
+// Local imports
+// ----------------------------------------------------------------------------
+
+@import "utilities/break";
+@import "utilities/convert";
+
+@import "config";
+
+@import "main/resets";
+@import "main/colors";
+@import "main/icons";
+@import "main/typeset";
+
+@import "main/components/author";
+@import "main/components/banner";
+@import "main/components/base";
+@import "main/components/clipboard";
+@import "main/components/consent";
+@import "main/components/content";
+@import "main/components/dialog";
+@import "main/components/feedback";
+@import "main/components/footer";
+@import "main/components/form";
+@import "main/components/header";
+@import "main/components/meta";
+@import "main/components/nav";
+@import "main/components/pagination";
+@import "main/components/post";
+@import "main/components/progress";
+@import "main/components/search";
+@import "main/components/select";
+@import "main/components/sidebar";
+@import "main/components/source";
+@import "main/components/status";
+@import "main/components/tabs";
+@import "main/components/tag";
+@import "main/components/tooltip";
+@import "main/components/top";
+@import "main/components/version";
+
+@import "main/extensions/markdown/admonition";
+@import "main/extensions/markdown/footnotes";
+@import "main/extensions/markdown/toc";
+
+@import "main/extensions/pymdownx/arithmatex";
+@import "main/extensions/pymdownx/critic";
+@import "main/extensions/pymdownx/details";
+@import "main/extensions/pymdownx/emoji";
+@import "main/extensions/pymdownx/highlight";
+@import "main/extensions/pymdownx/keys";
+@import "main/extensions/pymdownx/tabbed";
+@import "main/extensions/pymdownx/tasklist";
+
+@import "main/integrations/mermaid";
+
+@import "main/modifiers";
diff --git a/src/templates/assets/stylesheets/main/_colors.scss b/src/templates/assets/stylesheets/main/_colors.scss
new file mode 100644
index 00000000..68969fe9
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/_colors.scss
@@ -0,0 +1,153 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Color variables
+:root {
+ @extend %root;
+
+ // Primary color shades
+ --md-primary-fg-color: hsla(#{hex2hsl($clr-indigo-500)}, 1);
+ --md-primary-fg-color--light: hsla(#{hex2hsl($clr-indigo-400)}, 1);
+ --md-primary-fg-color--dark: hsla(#{hex2hsl($clr-indigo-700)}, 1);
+ --md-primary-bg-color: hsla(0, 0%, 100%, 1);
+ --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);
+
+ // Accent color shades
+ --md-accent-fg-color: hsla(#{hex2hsl($clr-indigo-a200)}, 1);
+ --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-indigo-a200)}, 0.1);
+ --md-accent-bg-color: hsla(0, 0%, 100%, 1);
+ --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7);
+}
+
+// ----------------------------------------------------------------------------
+
+// Allow to explicitly use color schemes in nested content
+[data-md-color-scheme="default"] {
+ @extend %root;
+
+ // Indicate that the site is rendered with a light color scheme
+ color-scheme: light;
+
+ // Hide images for dark mode
+ img[src$="#only-dark"],
+ img[src$="#gh-dark-mode-only"] {
+ display: none;
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Placeholders
+// ----------------------------------------------------------------------------
+
+// Default theme, i.e. light mode
+%root {
+
+ // Color hue in the range [0,360] - change this variable to alter the tone
+ // of the theme, e.g. to make it more redish or greenish
+ --md-hue: 225deg;
+
+ // Default color shades
+ --md-default-fg-color: hsla(0, 0%, 0%, 0.87);
+ --md-default-fg-color--light: hsla(0, 0%, 0%, 0.54);
+ --md-default-fg-color--lighter: hsla(0, 0%, 0%, 0.32);
+ --md-default-fg-color--lightest: hsla(0, 0%, 0%, 0.07);
+ --md-default-bg-color: hsla(0, 0%, 100%, 1);
+ --md-default-bg-color--light: hsla(0, 0%, 100%, 0.7);
+ --md-default-bg-color--lighter: hsla(0, 0%, 100%, 0.3);
+ --md-default-bg-color--lightest: hsla(0, 0%, 100%, 0.12);
+
+ // Code color shades
+ --md-code-fg-color: hsla(200, 18%, 26%, 1);
+ --md-code-bg-color: hsla(200, 0%, 96%, 1);
+
+ // Code highlighting color shades
+ --md-code-hl-color: hsla(#{hex2hsl($clr-blue-a200)}, 1);
+ --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a200)}, 0.1);
+ --md-code-hl-number-color: hsla(0, 67%, 50%, 1);
+ --md-code-hl-special-color: hsla(340, 83%, 47%, 1);
+ --md-code-hl-function-color: hsla(291, 45%, 50%, 1);
+ --md-code-hl-constant-color: hsla(250, 63%, 60%, 1);
+ --md-code-hl-keyword-color: hsla(219, 54%, 51%, 1);
+ --md-code-hl-string-color: hsla(150, 63%, 30%, 1);
+ --md-code-hl-name-color: var(--md-code-fg-color);
+ --md-code-hl-operator-color: var(--md-default-fg-color--light);
+ --md-code-hl-punctuation-color: var(--md-default-fg-color--light);
+ --md-code-hl-comment-color: var(--md-default-fg-color--light);
+ --md-code-hl-generic-color: var(--md-default-fg-color--light);
+ --md-code-hl-variable-color: var(--md-default-fg-color--light);
+
+ // Typeset color shades
+ --md-typeset-color: var(--md-default-fg-color);
+
+ // Typeset `a` color shades
+ --md-typeset-a-color: var(--md-primary-fg-color);
+
+ // Typeset `del` and `ins` color shades
+ --md-typeset-del-color: hsla(6, 90%, 60%, 0.15);
+ --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15);
+
+ // Typeset `kbd` color shades
+ --md-typeset-kbd-color: hsla(0, 0%, 98%, 1);
+ --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1);
+ --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1);
+
+ // Typeset `mark` color shades
+ --md-typeset-mark-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5);
+
+ // Typeset `table` color shades
+ --md-typeset-table-color: hsla(0, 0%, 0%, 0.12);
+ --md-typeset-table-color--light: hsla(0, 0%, 0%, 0.035);
+
+ // Admonition color shades
+ --md-admonition-fg-color: var(--md-default-fg-color);
+ --md-admonition-bg-color: var(--md-default-bg-color);
+
+ // Warning color shades
+ --md-warning-fg-color: hsla(0, 0%, 0%, 0.87);
+ --md-warning-bg-color: hsla(60, 100%, 80%, 1);
+
+ // Footer color shades
+ --md-footer-fg-color: hsla(0, 0%, 100%, 1);
+ --md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7);
+ --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.45);
+ --md-footer-bg-color: hsla(0, 0%, 0%, 0.87);
+ --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32);
+
+ // Shadow depth 1
+ --md-shadow-z1:
+ 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05),
+ 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1);
+
+ // Shadow depth 2
+ --md-shadow-z2:
+ 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.1),
+ 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25);
+
+ // Shadow depth 3
+ --md-shadow-z3:
+ 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.2),
+ 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35);
+}
diff --git a/src/templates/assets/stylesheets/main/_icons.scss b/src/templates/assets/stylesheets/main/_icons.scss
new file mode 100644
index 00000000..9853e93d
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/_icons.scss
@@ -0,0 +1,37 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Icon
+.md-icon {
+
+ // SVG defaults
+ svg {
+ display: block;
+ width: px2rem(24px);
+ height: px2rem(24px);
+ fill: currentcolor;
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/_modifiers.scss b/src/templates/assets/stylesheets/main/_modifiers.scss
new file mode 100644
index 00000000..4b2b046a
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/_modifiers.scss
@@ -0,0 +1,48 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // [tablet +]: Allow for rendering content as sidebars
+ @include break-from-device(tablet) {
+
+ // Modifier to float block elements
+ .inline {
+ float: inline-start;
+ width: px2rem(234px);
+ margin-inline-end: px2rem(16px);
+ margin-top: 0;
+ margin-bottom: px2rem(16px);
+
+ // Modifier to move to end (ltr: right, rtl: left)
+ &.end {
+ float: inline-end;
+ margin-inline: px2rem(16px) 0;
+ }
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/_resets.scss b/src/templates/assets/stylesheets/main/_resets.scss
new file mode 100644
index 00000000..c6fc4b28
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/_resets.scss
@@ -0,0 +1,118 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Enforce correct box model and prevent adjustments of font size after
+// orientation changes in IE and iOS
+html {
+ box-sizing: border-box;
+ text-size-adjust: none;
+}
+
+// All elements shall inherit the document default
+*,
+*::before,
+*::after {
+ box-sizing: inherit;
+
+ // [reduced motion]: Disable all transitions
+ @media (prefers-reduced-motion) {
+ transition: none !important; // stylelint-disable-line
+ }
+}
+
+// Remove margin in all browsers
+body {
+ margin: 0;
+}
+
+// Reset tap outlines on iOS and Android
+a,
+button,
+label,
+input {
+ -webkit-tap-highlight-color: transparent;
+}
+
+// Reset link styles
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+// Normalize horizontal separator styles
+hr {
+ box-sizing: content-box;
+ display: block;
+ height: px2rem(1px);
+ padding: 0;
+ overflow: visible;
+ border: 0;
+}
+
+// Normalize font-size in all browsers
+small {
+ font-size: 80%;
+}
+
+// Prevent subscript and superscript from affecting line-height
+sub,
+sup {
+ line-height: 1em;
+}
+
+// Remove border on image
+img {
+ border-style: none;
+}
+
+// Reset table styles
+table {
+ border-spacing: 0;
+ border-collapse: separate;
+}
+
+// Reset table cell styles
+td,
+th {
+ font-weight: 400;
+ vertical-align: top;
+}
+
+// Reset button styles
+button {
+ padding: 0;
+ margin: 0;
+ font-family: inherit;
+ font-size: inherit;
+ background: transparent;
+ border: 0;
+}
+
+// Reset input styles
+input {
+ border: 0;
+ outline: none;
+}
diff --git a/src/templates/assets/stylesheets/main/_typeset.scss b/src/templates/assets/stylesheets/main/_typeset.scss
new file mode 100644
index 00000000..1c322859
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/_typeset.scss
@@ -0,0 +1,603 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules: font definitions
+// ----------------------------------------------------------------------------
+
+// Enable font-smoothing in Webkit and FF
+body {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ // Font with fallback for body copy
+ --md-text-font-family:
+ var(--md-text-font, _),
+ -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
+
+ // Font with fallback for code
+ --md-code-font-family:
+ var(--md-code-font, _),
+ SFMono-Regular, Consolas, Menlo, monospace;
+}
+
+// Define default fonts
+body,
+input,
+aside {
+ font-family: var(--md-text-font-family);
+ font-feature-settings: "kern", "liga";
+ color: var(--md-typeset-color);
+}
+
+// Define monospaced fonts
+code,
+pre,
+kbd {
+ font-family: var(--md-code-font-family);
+ font-feature-settings: "kern";
+}
+
+// ----------------------------------------------------------------------------
+// Rules: typesetted content
+// ----------------------------------------------------------------------------
+
+// General variables
+:root {
+ --md-typeset-table-sort-icon: svg-load("material/sort.svg");
+ --md-typeset-table-sort-icon--asc: svg-load("material/sort-ascending.svg");
+ --md-typeset-table-sort-icon--desc: svg-load("material/sort-descending.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Content that is typeset - if possible, all margins, paddings and font sizes
+// should be set in ems, so nested blocks (e.g. admonitions) render correctly.
+.md-typeset {
+ font-size: px2rem(16px);
+ line-height: 1.6;
+ color-adjust: exact;
+
+ // [print]: We'll use a smaller `font-size` for printing, so code examples
+ // don't break too early, and `16px` looks too big anyway.
+ @media print {
+ font-size: px2rem(13.6px);
+ }
+
+ // Default spacing
+ ul,
+ ol,
+ dl,
+ figure,
+ blockquote,
+ pre {
+ margin-block: 1em;
+ }
+
+ // Headline on level 1
+ h1 {
+ margin: 0 0 px2em(40px, 32px);
+ font-size: px2em(32px);
+ font-weight: 300;
+ line-height: 1.3;
+ color: var(--md-default-fg-color--light);
+ letter-spacing: -0.01em;
+ }
+
+ // Headline on level 2
+ h2 {
+ margin: px2em(40px, 25px) 0 px2em(16px, 25px);
+ font-size: px2em(25px);
+ font-weight: 300;
+ line-height: 1.4;
+ letter-spacing: -0.01em;
+ }
+
+ // Headline on level 3
+ h3 {
+ margin: px2em(32px, 20px) 0 px2em(16px, 20px);
+ font-size: px2em(20px);
+ font-weight: 400;
+ line-height: 1.5;
+ letter-spacing: -0.01em;
+ }
+
+ // Headline on level 3 following level 2
+ h2 + h3 {
+ margin-top: px2em(16px, 20px);
+ }
+
+ // Headline on level 4
+ h4 {
+ margin: px2em(16px) 0;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+ }
+
+ // Headline on level 5-6
+ h5,
+ h6 {
+ margin: px2em(16px, 12.8px) 0;
+ font-size: px2em(12.8px);
+ font-weight: 700;
+ color: var(--md-default-fg-color--light);
+ letter-spacing: -0.01em;
+ }
+
+ // Headline on level 5
+ h5 {
+ text-transform: uppercase;
+ }
+
+ // Horizontal separator
+ hr {
+ display: flow-root;
+ margin: 1.5em 0;
+ border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest);
+ }
+
+ // Text link
+ a {
+ color: var(--md-typeset-a-color);
+ word-break: break-word;
+
+ // Also enable color transition on pseudo elements
+ &,
+ &::before {
+ transition: color 125ms;
+ }
+
+ // Text link on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+
+ // Inline code block
+ code {
+ background-color: var(--md-accent-fg-color--transparent);
+ }
+ }
+
+ // Inline code block
+ code {
+ color: currentcolor;
+ transition: background-color 125ms;
+ }
+
+ // Show outline for keyboard devices
+ &.focus-visible {
+ outline-color: var(--md-accent-fg-color);
+ outline-offset: px2rem(4px);
+ }
+ }
+
+ // Code block
+ code,
+ pre,
+ kbd {
+ font-variant-ligatures: none;
+ color: var(--md-code-fg-color);
+ direction: ltr;
+
+ // [print]: Wrap text and hide scollbars
+ @media print {
+ white-space: pre-wrap;
+ }
+ }
+
+ // Inline code block
+ code {
+ padding: 0 px2em(4px, 13.6px);
+ font-size: px2em(13.6px);
+ word-break: break-word;
+ background-color: var(--md-code-bg-color);
+ border-radius: px2rem(2px);
+ box-decoration-break: clone;
+
+ // Hide outline for pointer devices
+ &:not(.focus-visible) {
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ }
+ }
+
+ // Unformatted content
+ pre {
+ position: relative;
+ display: flow-root;
+ line-height: 1.4;
+
+ // Code block
+ > code {
+ display: block;
+ padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px);
+ margin: 0;
+ overflow: auto;
+ word-break: normal;
+ touch-action: auto;
+ outline-color: var(--md-accent-fg-color);
+ box-shadow: none;
+ box-decoration-break: slice;
+ scrollbar-width: thin;
+ scrollbar-color: var(--md-default-fg-color--lighter) transparent;
+
+ // Code block on hover
+ &:hover {
+ scrollbar-color: var(--md-accent-fg-color) transparent;
+ }
+
+ // Webkit scrollbar
+ &::-webkit-scrollbar {
+ width: px2rem(4px);
+ height: px2rem(4px);
+ }
+
+ // Webkit scrollbar thumb
+ &::-webkit-scrollbar-thumb {
+ background-color: var(--md-default-fg-color--lighter);
+
+ // Webkit scrollbar thumb on hover
+ &:hover {
+ background-color: var(--md-accent-fg-color);
+ }
+ }
+ }
+ }
+
+ // Keyboard key
+ kbd {
+ display: inline-block;
+ padding: 0 px2em(8px, 12px);
+ font-size: px2em(12px);
+ color: var(--md-default-fg-color);
+ word-break: break-word;
+ vertical-align: text-top;
+ background-color: var(--md-typeset-kbd-color);
+ border-radius: px2rem(2px);
+ box-shadow:
+ 0 px2rem(2px) 0 px2rem(1px) var(--md-typeset-kbd-border-color),
+ 0 px2rem(2px) 0 var(--md-typeset-kbd-border-color),
+ 0 px2rem(-2px) px2rem(4px) var(--md-typeset-kbd-accent-color) inset;
+ }
+
+ // Text highlighting marker
+ mark {
+ color: inherit;
+ word-break: break-word;
+ background-color: var(--md-typeset-mark-color);
+ box-decoration-break: clone;
+ }
+
+ // Abbreviation
+ abbr {
+ text-decoration: none;
+ cursor: help;
+ border-bottom: px2rem(1px) dotted var(--md-default-fg-color--light);
+
+ // Show tooltip for touch devices
+ @media (hover: none) {
+
+ // Tooltip
+ &[title]:is(:focus, :hover)::after {
+ position: absolute;
+ inset-inline: px2rem(16px);
+ padding: px2rem(4px) px2rem(6px);
+ margin-top: 2em;
+ font-size: px2rem(14px);
+ color: var(--md-default-bg-color);
+ content: attr(title);
+ background-color: var(--md-default-fg-color);
+ border-radius: px2rem(2px);
+ box-shadow: var(--md-shadow-z3);
+ }
+ }
+ }
+
+ // Small text
+ small {
+ opacity: 0.75;
+ }
+
+ // Superscript and subscript
+ sup,
+ sub {
+ margin-inline-start: px2em(1px, 12.8px);
+ }
+
+ // Blockquotes, possibly nested
+ blockquote {
+ padding-inline-start: px2rem(12px);
+ margin-inline: 0;
+ color: var(--md-default-fg-color--light);
+ border-inline-start: px2rem(4px) solid var(--md-default-fg-color--lighter);
+ }
+
+ // Unordered list
+ ul {
+ list-style-type: disc;
+ }
+
+ // Unordered and ordered list
+ ul,
+ ol {
+ padding: 0;
+ margin-inline-start: px2em(10px);
+
+ // Adjust display mode if not hidden
+ &:not([hidden]) {
+ display: flow-root;
+ }
+
+ // Nested ordered list
+ ol {
+ list-style-type: lower-alpha;
+
+ // Triply nested ordered list
+ ol {
+ list-style-type: lower-roman;
+ }
+ }
+
+ // List element
+ li {
+ margin-inline-start: px2em(20px);
+ margin-bottom: 0.5em;
+
+ // Adjust spacing
+ p,
+ blockquote {
+ margin: 0.5em 0;
+ }
+
+ // Adjust spacing on last child
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ // Nested list
+ :is(ul, ol) {
+ margin-block: 0.5em;
+ margin-inline-start: px2em(10px);
+ }
+ }
+ }
+
+ // Definition list
+ dd {
+ margin-block: 1em 1.5em;
+ margin-inline-start: px2em(30px);
+ }
+
+ // Image or video
+ img,
+ svg,
+ video {
+ max-width: 100%;
+ height: auto;
+ }
+
+ // Image
+ img {
+
+ // Adjust spacing when left-aligned
+ &[align="left"] {
+ margin: 1em;
+ margin-left: 0;
+ }
+
+ // Adjust spacing when right-aligned
+ &[align="right"] {
+ margin: 1em;
+ margin-right: 0;
+ }
+
+ // Adjust spacing when sole children
+ &[align]:only-child {
+ margin-top: 0;
+ }
+ }
+
+ // Figure
+ figure {
+ display: flow-root;
+ width: fit-content;
+ max-width: 100%;
+ margin: 1em auto;
+ text-align: center;
+
+ // Figure images
+ img {
+ display: block;
+ }
+ }
+
+ // Figure caption
+ figcaption {
+ max-width: px2rem(480px);
+ margin: 1em auto;
+ font-style: italic;
+ }
+
+ // Limit width to container
+ iframe {
+ max-width: 100%;
+ }
+
+ // Data table
+ table:not([class]) {
+ display: inline-block;
+ max-width: 100%;
+ overflow: auto;
+ font-size: px2rem(12.8px);
+ touch-action: auto;
+ background-color: var(--md-default-bg-color);
+ border: px2rem(1px) solid var(--md-typeset-table-color);
+ border-radius: px2rem(2px);
+
+ // [print]: Reset display mode so table header wraps when printing
+ @media print {
+ display: table;
+ }
+
+ // Due to margin collapse because of the necessary inline-block hack, we
+ // cannot increase the bottom margin on the table, so we just increase the
+ // top margin on the following element
+ + * {
+ margin-top: 1.5em;
+ }
+
+ // Elements in table heading and cell
+ :is(th, td) > * {
+
+ // Adjust spacing on first child
+ &:first-child {
+ margin-top: 0;
+ }
+
+ // Adjust spacing on last child
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ // Table heading and cell
+ :is(th, td):not([align]) {
+ text-align: left;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ text-align: right;
+ }
+ }
+
+ // Table heading
+ th {
+ min-width: px2rem(100px);
+ padding: px2em(12px, 12.8px) px2em(16px, 12.8px);
+ font-weight: 700;
+ vertical-align: top;
+ }
+
+ // Table cell
+ td {
+ padding: px2em(12px, 12.8px) px2em(16px, 12.8px);
+ vertical-align: top;
+ border-top: px2rem(1px) solid var(--md-typeset-table-color);
+ }
+
+ // Table body row
+ tbody tr {
+ transition: background-color 125ms;
+
+ // Table row on hover
+ &:hover {
+ background-color: var(--md-typeset-table-color--light);
+ box-shadow: 0 px2rem(1px) 0 var(--md-default-bg-color) inset;
+ }
+ }
+
+ // Text link in table
+ a {
+ word-break: normal;
+ }
+ }
+
+ // Sortable table
+ table th[role="columnheader"] {
+ cursor: pointer;
+
+ // Sort icon
+ &::after {
+ display: inline-block;
+ width: 1.2em;
+ height: 1.2em;
+ margin-inline-start: 0.5em;
+ vertical-align: text-bottom;
+ content: "";
+ transition: background-color 125ms;
+ mask-image: var(--md-typeset-table-sort-icon);
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ // Show sort icon on hover
+ &:hover::after {
+ background-color: var(--md-default-fg-color--lighter);
+ }
+
+ // Sort ascending icon
+ &[aria-sort="ascending"]::after {
+ background-color: var(--md-default-fg-color--light);
+ mask-image: var(--md-typeset-table-sort-icon--asc);
+ }
+
+ // Sort descending icon
+ &[aria-sort="descending"]::after {
+ background-color: var(--md-default-fg-color--light);
+ mask-image: var(--md-typeset-table-sort-icon--desc);
+ }
+ }
+
+ // Data table scroll wrapper
+ &__scrollwrap {
+ margin: 1em px2rem(-16px);
+ overflow-x: auto;
+ touch-action: auto;
+ }
+
+ // Data table wrapper
+ &__table {
+ display: inline-block;
+ padding: 0 px2rem(16px);
+ margin-bottom: 0.5em;
+
+ // [print]: Reset display mode so table header wraps when printing
+ @media print {
+ display: block;
+ }
+
+ // Data table
+ html & table {
+ display: table;
+ width: 100%;
+ margin: 0;
+ overflow: hidden;
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules: top-level
+// ----------------------------------------------------------------------------
+
+// [mobile -]: Align with body copy
+@include break-to-device(mobile) {
+
+ // Top-level unformatted content
+ .md-content__inner > pre {
+ margin: 1em px2rem(-16px);
+
+ // Code block
+ code {
+ border-radius: 0;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_author.scss b/src/templates/assets/stylesheets/main/components/_author.scss
new file mode 100644
index 00000000..111baf40
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_author.scss
@@ -0,0 +1,86 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Author, i.e., GitHub user
+ .md-author {
+ position: relative;
+ display: block;
+ flex-shrink: 0;
+ width: px2rem(32px);
+ height: px2rem(32px);
+ overflow: hidden;
+ transition:
+ color 125ms,
+ transform 125ms;
+
+ // Author image
+ img {
+ display: block;
+ border-radius: 100%;
+ }
+
+ // More authors
+ &--more {
+ font-size: px2rem(12px);
+ font-weight: 700;
+ line-height: px2rem(32px);
+ color: var(--md-default-fg-color--lighter);
+ text-align: center;
+ background: var(--md-default-fg-color--lightest);
+ }
+
+ // Enlarge image
+ &--long {
+ width: px2rem(48px);
+ height: px2rem(48px);
+ }
+ }
+
+ // Author link
+ a.md-author {
+ transform: scale(1);
+
+ // Author image
+ img {
+ filter: grayscale(100%) opacity(75%);
+ transition: filter 125ms;
+ }
+
+ // Author on focus/hover
+ &:is(:focus, :hover) {
+ z-index: 1;
+ transform: scale(1.1);
+
+ // Author image
+ img {
+ filter: grayscale(0%);
+ }
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_banner.scss b/src/templates/assets/stylesheets/main/components/_banner.scss
new file mode 100644
index 00000000..8fe08c0f
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_banner.scss
@@ -0,0 +1,68 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Banner for announcements and warnings
+.md-banner {
+ overflow: auto;
+ color: var(--md-footer-fg-color);
+ background-color: var(--md-footer-bg-color);
+
+ // [print]: Hide banner
+ @media print {
+ display: none;
+ }
+
+ // Banner with warning
+ &--warning {
+ color: var(--md-warning-fg-color);
+ background-color: var(--md-warning-bg-color);
+ }
+
+ // Banner wrapper
+ &__inner {
+ padding: 0 px2rem(16px);
+ margin: px2rem(12px) auto;
+ font-size: px2rem(14px);
+ }
+
+ // Banner button
+ &__button {
+ float: inline-end;
+ color: inherit;
+ cursor: pointer;
+ transition: opacity 250ms;
+
+ // [no-js]: Hide button
+ .no-js & {
+ display: none;
+ }
+
+ // Button on hover
+ &:hover {
+ opacity: 0.7;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_base.scss b/src/templates/assets/stylesheets/main/components/_base.scss
new file mode 100644
index 00000000..33f834ed
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_base.scss
@@ -0,0 +1,182 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules: base grid and containers
+// ----------------------------------------------------------------------------
+
+// Stretch container to viewport and set base `font-size`
+html {
+ height: 100%;
+ overflow-x: hidden;
+ // Hack: normally, we would set the base `font-size` to `62.5%`, so we can
+ // base all calculations on `10px`, but Chromium and Chrome define a minimal
+ // `font-size` of `12px` if the system language is set to Chinese. For this
+ // reason we just double the `font-size` and set it to `20px`.
+ //
+ // See https://github.com/squidfunk/mkdocs-material/issues/911
+ font-size: 125%;
+
+ // [screen medium +]: Set base `font-size` to `11px`
+ @include break-from-device(screen medium) {
+ font-size: 137.5%;
+ }
+
+ // [screen large +]: Set base `font-size` to `12px`
+ @include break-from-device(screen large) {
+ font-size: 150%;
+ }
+}
+
+// Stretch body to container - flexbox is used, so the footer will always be
+// aligned to the bottom of the viewport
+body {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ min-height: 100%;
+ // Hack: reset `font-size` to `10px`, so the spacing for all inline elements
+ // is correct again. Otherwise the spacing would be based on `20px`.
+ font-size: px2rem(10px);
+ background-color: var(--md-default-bg-color);
+
+ // [print]: Omit flexbox layout due to a Firefox bug (https://mzl.la/39DgR3m)
+ @media print {
+ display: block;
+ }
+
+ // Body in locked state
+ &[data-md-scrolllock] {
+
+ // [tablet portrait -]: Omit scroll bubbling
+ @include break-to-device(tablet portrait) {
+ position: fixed;
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+// Grid container - this class is applied to wrapper elements within the
+// header, content area and footer, and makes sure that their width is limited
+// to `1220px`, and they are rendered centered if the screen is larger.
+.md-grid {
+ max-width: px2rem(1220px);
+ margin-inline: auto;
+}
+
+// Main container
+.md-container {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+
+ // [print]: Omit flexbox layout due to a Firefox bug (https://mzl.la/39DgR3m)
+ @media print {
+ display: block;
+ }
+}
+
+// Main area - stretch to remaining space of container
+.md-main {
+ flex-grow: 1;
+
+ // Main area wrapper
+ &__inner {
+ display: flex;
+ height: 100%;
+ margin-top: px2rem(24px + 6px);
+ }
+}
+
+// Add ellipsis in case of overflowing text
+.md-ellipsis {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+// ----------------------------------------------------------------------------
+// Rules: navigational elements
+// ----------------------------------------------------------------------------
+
+// Toggle - this class is applied to checkbox elements, which are used to
+// implement the CSS-only drawer and navigation, as well as the search
+.md-toggle {
+ display: none;
+}
+
+// Option - this class is applied to radio elements, which are used to
+// implement the color palette toggle
+.md-option {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+
+ // Option label for checked radio button
+ &:checked + label:not([hidden]) {
+ display: block;
+ }
+
+ // Show outline for keyboard devices
+ &.focus-visible + label {
+ outline-style: auto;
+ outline-color: var(--md-accent-fg-color);
+ }
+}
+
+// Skip link
+.md-skip {
+ position: fixed;
+ // Hack: if we don't set the negative `z-index`, the skip link will force the
+ // creation of new layers when code blocks are near the header on scrolling
+ z-index: -1;
+ padding: px2rem(6px) px2rem(10px);
+ margin: px2rem(10px);
+ font-size: px2rem(12.8px);
+ color: var(--md-default-bg-color);
+ background-color: var(--md-default-fg-color);
+ border-radius: px2rem(2px);
+ outline-color: var(--md-accent-fg-color);
+ opacity: 0;
+ transform: translateY(px2rem(8px));
+
+ // Show skip link on focus
+ &:focus {
+ z-index: 10;
+ opacity: 1;
+ transition:
+ transform 250ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 175ms 75ms;
+ transform: translateY(0);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules: print styles
+// ----------------------------------------------------------------------------
+
+// Add margins to page
+@page {
+ margin: 25mm;
+}
diff --git a/src/templates/assets/stylesheets/main/components/_clipboard.scss b/src/templates/assets/stylesheets/main/components/_clipboard.scss
new file mode 100644
index 00000000..c07c9c67
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_clipboard.scss
@@ -0,0 +1,102 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Clipboard button variables
+:root {
+ --md-clipboard-icon: svg-load("material/content-copy.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Clipboard button
+.md-clipboard {
+ position: absolute;
+ top: px2em(8px);
+ right: px2em(8px);
+ z-index: 1;
+ width: px2em(24px);
+ height: px2em(24px);
+ color: var(--md-default-fg-color--lightest);
+ cursor: pointer;
+ border-radius: px2rem(2px);
+ outline-color: var(--md-accent-fg-color);
+ outline-offset: px2rem(2px);
+ transition: color 250ms;
+
+ // [print]: Hide button
+ @media print {
+ display: none;
+ }
+
+ // Hide outline for pointer devices
+ &:not(.focus-visible) {
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ // Darken color on code block hover
+ :hover > & {
+ color: var(--md-default-fg-color--light);
+ }
+
+ // Button on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+ }
+
+ // Button icon - the width and height are defined in `em`, so the size is
+ // automatically adjusted for nested code blocks (e.g. in admonitions)
+ &::after {
+ display: block;
+ width: px2em(18px);
+ height: px2em(18px);
+ margin: 0 auto;
+ content: "";
+ background-color: currentcolor;
+ mask-image: var(--md-clipboard-icon);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ // Inline clipboard button
+ &--inline {
+ cursor: pointer;
+
+ // Code block
+ code {
+ transition:
+ color 250ms,
+ background-color 250ms;
+ }
+
+ // Code block on focus/hover
+ &:is(:focus, :hover) code {
+ color: var(--md-accent-fg-color);
+ background-color: var(--md-accent-fg-color--transparent);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_consent.scss b/src/templates/assets/stylesheets/main/components/_consent.scss
new file mode 100644
index 00000000..5502460c
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_consent.scss
@@ -0,0 +1,127 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Keyframes
+// ----------------------------------------------------------------------------
+
+// Show consent
+@keyframes consent {
+ 0% {
+ opacity: 0;
+ transform: translateY(100%);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+// Show consent overlay
+@keyframes overlay {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Consent
+.md-consent {
+
+ // Consent overlay
+ &__overlay {
+ position: fixed;
+ top: 0;
+ z-index: 5;
+ width: 100%;
+ height: 100%;
+ background-color: hsla(0, 0%, 0%, 0.54);
+ opacity: 1;
+ backdrop-filter: blur(px2rem(2px));
+ animation: overlay 250ms both;
+ }
+
+ // Consent wrapper
+ &__inner {
+ position: fixed;
+ bottom: 0;
+ z-index: 5;
+ width: 100%;
+ max-height: 100%;
+ padding: 0;
+ overflow: auto;
+ background-color: var(--md-default-bg-color);
+ border: 0;
+ border-radius: px2rem(2px);
+ box-shadow:
+ 0 0 px2rem(4px) rgba(0, 0, 0, 0.1),
+ 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2);
+ animation: consent 500ms cubic-bezier(0.1, 0.7, 0.1, 1) both;
+ }
+
+ // Consent form
+ &__form {
+ padding: px2rem(16px);
+ }
+
+ // Consent settings
+ &__settings {
+ display: none;
+ margin: 1em 0;
+
+ // Show settings
+ input:checked + & {
+ display: block;
+ }
+ }
+
+ // Consent controls
+ &__controls {
+ margin-bottom: px2rem(16px);
+
+ // Consent control button
+ .md-typeset & .md-button {
+ display: inline;
+
+ // [tablet +]: Align buttons horizontally
+ @include break-to-device(mobile) {
+ display: block;
+ width: 100%;
+ margin-top: px2rem(8px);
+ text-align: center;
+ }
+ }
+ }
+
+ // Ensure users realize that labels are clickaböe
+ label {
+ cursor: pointer;
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_content.scss b/src/templates/assets/stylesheets/main/components/_content.scss
new file mode 100644
index 00000000..7c945749
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_content.scss
@@ -0,0 +1,97 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Content area
+.md-content {
+ flex-grow: 1;
+ // Hack: we must use `min-width: 0`, so the content area is capped by the
+ // dimensions of its parent. Otherwise, long code blocks might lead to a
+ // wider content area which will overflow. See https://bit.ly/3bP3f8k
+ min-width: 0;
+
+ // Content wrapper
+ &__inner {
+ padding-top: px2rem(12px);
+ margin: 0 px2rem(16px) px2rem(24px);
+
+ // [screen +]: Adjust spacing between content area and sidebars
+ @include break-from-device(screen) {
+
+ // Sidebar with navigation is visible
+ .md-sidebar--primary:not([hidden]) ~ .md-content > & {
+ margin-inline-start: px2rem(24px);
+ }
+
+ // Sidebar with table of contents is visible
+ .md-sidebar--secondary:not([hidden]) ~ .md-content > & {
+ margin-inline-end: px2rem(24px);
+ }
+ }
+
+ // Hack: add pseudo element for spacing, as the overflow of the content
+ // container may not be hidden due to an imminent offset error on targets
+ &::before {
+ display: block;
+ height: px2rem(8px);
+ content: "";
+ }
+
+ // Adjust spacing on last child
+ > :last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ // Button inside of the content area - these buttons are meant for actions on
+ // a document-level, i.e. linking to related source code files, printing etc.
+ &__button {
+ float: inline-end;
+ padding: 0;
+ margin: px2rem(8px) 0;
+ margin-inline-start: px2rem(8px);
+
+ // [print]: Hide buttons
+ @media print {
+ display: none;
+ }
+
+ // Adjust default link color for icons
+ .md-typeset & {
+ color: var(--md-default-fg-color--lighter);
+ }
+
+ // Align with body copy located next to icon
+ svg {
+ display: inline;
+ vertical-align: top;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: scaleX(-1);
+ }
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_dialog.scss b/src/templates/assets/stylesheets/main/components/_dialog.scss
new file mode 100644
index 00000000..16782ede
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_dialog.scss
@@ -0,0 +1,65 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Dialog
+.md-dialog {
+ position: fixed;
+ bottom: px2rem(16px);
+ z-index: 4;
+ min-width: px2rem(222px);
+ padding: px2rem(8px) px2rem(12px);
+ pointer-events: none;
+ background-color: var(--md-default-fg-color);
+ border-radius: px2rem(2px);
+ box-shadow: var(--md-shadow-z3);
+ opacity: 0;
+ transition:
+ transform 0ms 400ms,
+ opacity 400ms;
+ transform: translateY(100%);
+ inset-inline-end: px2rem(16px);
+
+ // [print]: Hide dialog
+ @media print {
+ display: none;
+ }
+
+ // Active dialog
+ &--active {
+ pointer-events: initial;
+ opacity: 1;
+ transition:
+ transform 400ms cubic-bezier(0.075, 0.85, 0.175, 1),
+ opacity 400ms;
+ transform: translateY(0);
+ }
+
+ // Dialog wrapper
+ &__inner {
+ font-size: px2rem(14px);
+ color: var(--md-default-bg-color);
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_feedback.scss b/src/templates/assets/stylesheets/main/components/_feedback.scss
new file mode 100644
index 00000000..bbcd00e9
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_feedback.scss
@@ -0,0 +1,110 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Was this page helpful?
+.md-feedback {
+ margin: 2em 0 1em;
+ text-align: center;
+
+ // Feedback fieldset
+ fieldset {
+ padding: 0;
+ margin: 0;
+ border: none;
+ }
+
+ // Feedback title
+ &__title {
+ margin: 1em auto;
+ font-weight: 700;
+ }
+
+ // Feedback wrapper
+ &__inner {
+ position: relative;
+ }
+
+ // Feedback list
+ &__list {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-content: baseline;
+ justify-content: center;
+
+ // Feedback icon on hover
+ &:hover .md-icon:not(:disabled) {
+ color: var(--md-default-fg-color--lighter);
+ }
+
+ // Adjust height after submission
+ :disabled & {
+ min-height: px2rem(36px);
+ }
+ }
+
+ // Feedback icon
+ &__icon {
+ flex-shrink: 0;
+ margin: 0 px2rem(2px);
+ color: var(--md-default-fg-color--light);
+ cursor: pointer;
+ transition: color 125ms;
+
+ // Feedback icon on hover
+ &:not(:disabled).md-icon:hover {
+ color: var(--md-accent-fg-color);
+ }
+
+ // Feedback icon after submit
+ &:disabled {
+ color: var(--md-default-fg-color--lightest);
+ pointer-events: none;
+ }
+ }
+
+ // Feedback note
+ &__note {
+ position: relative;
+ opacity: 0;
+ transition:
+ transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),
+ opacity 150ms;
+ transform: translateY(px2rem(8px));
+
+ // Feedback note value
+ > * {
+ max-width: px2rem(320px);
+ margin: 0 auto;
+ }
+
+ // Show after submission
+ :disabled & {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_footer.scss b/src/templates/assets/stylesheets/main/components/_footer.scss
new file mode 100644
index 00000000..9fabc05b
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_footer.scss
@@ -0,0 +1,201 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Footer
+.md-footer {
+ color: var(--md-footer-fg-color);
+ background-color: var(--md-footer-bg-color);
+
+ // [print]: Hide footer
+ @media print {
+ display: none;
+ }
+
+ // Footer wrapper
+ &__inner {
+ justify-content: space-between;
+ padding: px2rem(4px);
+ overflow: auto;
+
+ // Footer is visible
+ &:not([hidden]) {
+ display: flex;
+ }
+ }
+
+ // Footer link to previous and next page
+ &__link {
+ display: flex;
+ // Hack: some browsers induce ellipsis on flex children that are set to
+ // `overflow: hidden` and `text-overflow: ellipsis`. Enforcing growth by
+ // a tiny factor seems to get rid of the ellipsis and renders the text as
+ // it should - see https://bit.ly/2ZUCXQ8
+ flex-grow: 0.01;
+ align-items: end;
+ max-width: 100%;
+ margin-block: px2rem(20px) px2rem(8px);
+ overflow: hidden;
+ outline-color: var(--md-accent-fg-color);
+ transition: opacity 250ms;
+
+ // Footer link on focus/hover
+ &:is(:focus, :hover) {
+ opacity: 0.7;
+ }
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & svg {
+ transform: scaleX(-1);
+ }
+
+ // [mobile -]: Adjust width to 25/75 and hide title
+ @include break-to-device(mobile) {
+
+ // Footer link to previous page
+ &--prev {
+ flex-shrink: 0;
+
+ // Hide footer title
+ .md-footer__title {
+ display: none;
+ }
+ }
+ }
+
+ // Footer link to next page
+ &--next {
+ margin-inline-start: auto;
+ text-align: right;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ text-align: left;
+ }
+ }
+ }
+
+ // Footer title
+ &__title {
+ flex-grow: 1;
+ max-width: calc(100% - #{px2rem(48px)});
+ padding: 0 px2rem(20px);
+ margin-bottom: px2rem(14px);
+ font-size: px2rem(18px);
+ white-space: nowrap;
+ }
+
+ // Footer link button
+ &__button {
+ padding: px2rem(8px);
+ margin: px2rem(4px);
+ }
+
+ // Footer link direction (i.e. prev and next)
+ &__direction {
+ font-size: px2rem(12.8px);
+ opacity: 0.7;
+ }
+}
+
+// Footer metadata
+.md-footer-meta {
+ background-color: var(--md-footer-bg-color--dark);
+
+ // Footer metadata wrapper
+ &__inner {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ padding: px2rem(4px);
+ }
+
+ // Lighten color for non-hovered text links
+ html &.md-typeset a {
+ color: var(--md-footer-fg-color--light);
+
+ // Text link on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-footer-fg-color);
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+// Copyright and theme information
+.md-copyright {
+ width: 100%;
+ padding: px2rem(8px) 0;
+ margin: auto px2rem(12px);
+ font-size: px2rem(12.8px);
+ color: var(--md-footer-fg-color--lighter);
+
+ // [tablet portrait +]: Show copyright and social links in one line
+ @include break-from-device(tablet portrait) {
+ width: auto;
+ }
+
+ // Footer copyright highlight - this is the upper part of the copyright and
+ // theme information, which will include a darker color than the theme link
+ &__highlight {
+ color: var(--md-footer-fg-color--light);
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+// Social links
+.md-social {
+ display: inline-flex;
+ gap: px2rem(4px);
+ padding: px2rem(4px) 0 px2rem(12px);
+ margin: 0 px2rem(8px);
+
+ // [tablet portrait +]: Show copyright and social links in one line
+ @include break-from-device(tablet portrait) {
+ padding: px2rem(12px) 0;
+ }
+
+ // Footer social link
+ &__link {
+ display: inline-block;
+ width: px2rem(32px);
+ height: px2rem(32px);
+ text-align: center;
+
+ // Adjust line-height to match height for correct alignment
+ &::before {
+ line-height: 1.9;
+ }
+
+ // Fill icon with current color
+ svg {
+ max-height: px2rem(16px);
+ vertical-align: -25%;
+ fill: currentcolor;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_form.scss b/src/templates/assets/stylesheets/main/components/_form.scss
new file mode 100644
index 00000000..49b59e42
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_form.scss
@@ -0,0 +1,83 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Form button
+ .md-button {
+ display: inline-block;
+ padding: px2em(10px) px2em(32px);
+ font-weight: 700;
+ color: var(--md-primary-fg-color);
+ cursor: pointer;
+ border: px2rem(2px) solid currentcolor;
+ border-radius: px2rem(2px);
+ transition:
+ color 125ms,
+ background-color 125ms,
+ border-color 125ms;
+
+ // Primary button
+ &--primary {
+ color: var(--md-primary-bg-color);
+ background-color: var(--md-primary-fg-color);
+ border-color: var(--md-primary-fg-color);
+ }
+
+ // Button on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-bg-color);
+ background-color: var(--md-accent-fg-color);
+ border-color: var(--md-accent-fg-color);
+ }
+ }
+
+ // Form input
+ .md-input {
+ height: px2rem(36px);
+ padding: 0 px2rem(12px);
+ font-size: px2rem(16px);
+ border-bottom: px2rem(2px) solid var(--md-default-fg-color--lighter);
+ border-start-start-radius: px2rem(2px);
+ border-start-end-radius: px2rem(2px);
+ box-shadow: var(--md-shadow-z1);
+ transition:
+ border 250ms,
+ box-shadow 250ms;
+
+ // Input on focus/hover
+ &:is(:focus, :hover) {
+ border-bottom-color: var(--md-accent-fg-color);
+ box-shadow: var(--md-shadow-z2);
+ }
+
+ // Stretch to full width
+ &--stretch {
+ width: 100%;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_header.scss b/src/templates/assets/stylesheets/main/components/_header.scss
new file mode 100644
index 00000000..e51f3f99
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_header.scss
@@ -0,0 +1,270 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Header - by default, the header will be sticky and stay always on top of the
+// viewport. If this behavior is not desired, just set `position: static`.
+.md-header {
+ position: sticky;
+ inset-inline: 0;
+ top: 0;
+ z-index: 4;
+ display: block;
+ color: var(--md-primary-bg-color);
+ background-color: var(--md-primary-fg-color);
+ // Hack: reduce jitter by adding a transparent box shadow of the same size
+ // so the size of the layer doesn't change during animation
+ box-shadow:
+ 0 0 px2rem(4px) rgba(0, 0, 0, 0),
+ 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0);
+
+ // [print]: Hide header
+ @media print {
+ display: none;
+ }
+
+ // Header is hidden
+ &[hidden] {
+ transition:
+ transform 250ms cubic-bezier(0.8, 0, 0.6, 1),
+ box-shadow 250ms;
+ transform: translateY(-100%);
+ }
+
+ // Header in shadow state, i.e. shadow is visible
+ &--shadow {
+ box-shadow:
+ 0 0 px2rem(4px) rgba(0, 0, 0, 0.1),
+ 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2);
+ transition:
+ transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1),
+ box-shadow 250ms;
+ }
+
+ // Header wrapper
+ &__inner {
+ display: flex;
+ align-items: center;
+ padding: 0 px2rem(4px);
+ }
+
+ // Header button
+ &__button {
+ position: relative;
+ z-index: 1;
+ padding: px2rem(8px);
+ margin: px2rem(4px);
+ color: currentcolor;
+ vertical-align: middle;
+ cursor: pointer;
+ outline-color: var(--md-accent-fg-color);
+ transition: opacity 250ms;
+
+ // Button on hover
+ &:hover {
+ opacity: 0.7;
+ }
+
+ // Header button is visible
+ &:not([hidden]) {
+ display: inline-block;
+ }
+
+ // Hide outline for pointer devices
+ &:not(.focus-visible) {
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ // Button with logo, pointing to `config.site_url`
+ &.md-logo {
+ padding: px2rem(8px);
+ margin: px2rem(4px);
+
+ // [tablet -]: Hide button
+ @include break-to-device(tablet) {
+ display: none;
+ }
+
+ // Image or icon
+ :is(img, svg) {
+ display: block;
+ width: auto;
+ height: px2rem(24px);
+ fill: currentcolor;
+ }
+ }
+
+ // Button for search
+ &[for="__search"] {
+
+ // [tablet landscape +]: Hide button
+ @include break-from-device(tablet landscape) {
+ display: none;
+ }
+
+ // [no-js]: Hide button
+ .no-js & {
+ display: none;
+ }
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & svg {
+ transform: scaleX(-1);
+ }
+ }
+
+ // Button for drawer
+ &[for="__drawer"] {
+
+ // [screen +]: Hide button
+ @include break-from-device(screen) {
+ display: none;
+ }
+ }
+ }
+
+ // Header topic
+ &__topic {
+ position: absolute;
+ display: flex;
+ max-width: 100%;
+ white-space: nowrap;
+ transition:
+ transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),
+ opacity 150ms;
+
+ // Second header topic - title of the current page
+ & + & {
+ z-index: -1;
+ pointer-events: none;
+ opacity: 0;
+ transition:
+ transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1),
+ opacity 150ms;
+ transform: translateX(px2rem(25px));
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: translateX(px2rem(-25px));
+ }
+ }
+
+ // Adjust font weight of site title
+ &:first-child {
+ font-weight: 700;
+ }
+ }
+
+ // Header title
+ &__title {
+ flex-grow: 1;
+ height: px2rem(48px);
+ margin-inline-start: px2rem(20px);
+ margin-inline-end: px2rem(8px);
+ font-size: px2rem(18px);
+ line-height: px2rem(48px);
+
+ // Header title in active state, i.e. page title is visible
+ &--active .md-header__topic {
+ z-index: -1;
+ pointer-events: none;
+ opacity: 0;
+ transition:
+ transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1),
+ opacity 150ms;
+ transform: translateX(px2rem(-25px));
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: translateX(px2rem(25px));
+ }
+
+ // Second header topic - title of the current page
+ + .md-header__topic {
+ z-index: 0;
+ pointer-events: initial;
+ opacity: 1;
+ transition:
+ transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),
+ opacity 150ms;
+ transform: translateX(0);
+ }
+ }
+
+ // Add ellipsis in case of overflowing text
+ > .md-header__ellipsis {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ // Header option
+ &__option {
+ display: flex;
+ flex-shrink: 0;
+ max-width: 100%;
+ white-space: nowrap;
+ transition:
+ max-width 0ms 250ms,
+ opacity 250ms 250ms;
+
+ // Hide toggle when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+ max-width: 0;
+ opacity: 0;
+ transition:
+ max-width 0ms,
+ opacity 0ms;
+ }
+
+ // Hack: Firefox 117 introduces a bug where the browser scrolls the page by
+ // a small amount to the top every time the header button is focused. After
+ // investigating, we're confident that it seems to be caused by the input
+ // field being too close to the border - see https://t.ly/APO8l
+ > input {
+ bottom: 0;
+ }
+ }
+
+ // Repository information container
+ &__source {
+ display: none;
+
+ // [tablet landscape +]: Show repository information
+ @include break-from-device(tablet landscape) {
+ display: block;
+ width: px2rem(234px);
+ max-width: px2rem(234px);
+ margin-inline-start: px2rem(20px);
+ }
+
+ // [screen +]: Adjust spacing of search bar
+ @include break-from-device(screen) {
+ margin-inline-start: px2rem(28px);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_meta.scss b/src/templates/assets/stylesheets/main/components/_meta.scss
new file mode 100644
index 00000000..aaeae8df
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_meta.scss
@@ -0,0 +1,67 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Metadata
+.md-meta {
+ font-size: px2rem(14px);
+ line-height: 1.3;
+ color: var(--md-default-fg-color--light);
+
+ // Metadata list
+ &__list {
+ display: inline-flex;
+ flex-wrap: wrap;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ }
+
+ // Metadata item separator
+ &__item:not(:last-child)::after {
+ margin-inline: px2rem(4px);
+ content: "·";
+ }
+
+ // Metadata link
+ &__link {
+ color: var(--md-typeset-a-color);
+
+ // Metadata link on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+ }
+ }
+}
+
+// Draft
+.md-draft {
+ display: inline-block;
+ padding-inline: px2em(8px, 14px);
+ font-weight: 700;
+ color: hsla(255, 100%, 100%);
+ background-color: $clr-red-a400;
+ border-radius: px2em(2px);
+}
diff --git a/src/templates/assets/stylesheets/main/components/_nav.scss b/src/templates/assets/stylesheets/main/components/_nav.scss
new file mode 100644
index 00000000..673918af
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_nav.scss
@@ -0,0 +1,761 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Navigation variables
+:root {
+ --md-nav-icon--prev: svg-load("material/arrow-left.svg");
+ --md-nav-icon--next: svg-load("material/chevron-right.svg");
+ --md-toc-icon: svg-load("material/table-of-contents.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Navigation
+.md-nav {
+ font-size: px2rem(14px);
+ line-height: 1.3;
+
+ // Navigation title
+ &__title {
+ display: block;
+ padding: 0 px2rem(12px);
+ overflow: hidden;
+ font-weight: 700;
+ color: var(--md-default-fg-color--light);
+ text-overflow: ellipsis;
+
+ // Navigaton button
+ .md-nav__button {
+ display: none;
+
+ // Stretch images based on height, as it's the smaller dimension
+ img {
+ width: auto;
+ height: 100%;
+ }
+
+ // Button with logo, pointing to `config.site_url`
+ &.md-logo {
+
+ // Image or icon
+ :is(img, svg) {
+ display: block;
+ width: auto;
+ max-width: 100%;
+ height: px2rem(48px);
+ object-fit: contain;
+ fill: currentcolor;
+ }
+ }
+ }
+ }
+
+ // Navigation list
+ &__list {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ }
+
+ // Navigation link
+ &__link {
+ display: flex;
+ gap: px2rem(8px);
+ align-items: flex-start;
+ margin-top: 0.625em;
+ transition: color 125ms;
+ scroll-snap-align: start;
+
+ // Navigation link that was passed
+ &--passed {
+ color: var(--md-default-fg-color--light);
+ }
+
+ // Active link
+ .md-nav__item &--active {
+
+ // Also enable color transitions on inline code blocks
+ &,
+ code {
+ color: var(--md-typeset-a-color);
+ }
+ }
+
+ // Navigation link title
+ .md-ellipsis {
+ // Hack: Safari exhibits a bug where the text will sometimes disappear
+ // and the element will become unclickable. Setting `position: relative`
+ // seems to fix the issue - see https://bit.ly/3HljM1T
+ position: relative;
+ }
+
+ // Always align navigation icons to the end
+ .md-icon:last-child {
+ margin-inline-start: auto;
+ }
+
+ // Navigation link icon
+ svg {
+ flex-shrink: 0;
+ height: 1.3em;
+ fill: currentcolor;
+ }
+
+ // Navigation link on focus/hover
+ &:is([href], [for]):is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+ cursor: pointer;
+ }
+
+ // Show outline for keyboard devices
+ &.focus-visible {
+ outline-color: var(--md-accent-fg-color);
+ outline-offset: px2rem(4px);
+ }
+
+ // Navigation link for table of contents
+ .md-nav--primary &[for="__toc"] {
+ display: none;
+
+ // Table of contents icon
+ .md-icon::after {
+ display: block;
+ width: 100%;
+ height: 100%;
+ mask-image: var(--md-toc-icon);
+ background-color: currentcolor;
+ }
+
+ // Hide table of contents
+ ~ .md-nav {
+ display: none;
+ }
+ }
+ }
+
+ // Navigation container (for section index pages)
+ &__container > .md-nav__link {
+ margin-top: 0;
+
+ // Stretch first child
+ &:first-child {
+ flex-grow: 1;
+ // Hack: if a very long word is used, it can push the arrow out of sight.
+ // Setting this property contains the text - see https://t.ly/E02vp
+ min-width: 0;
+ }
+ }
+
+ // Navigation icon
+ &__icon {
+ flex-shrink: 0;
+ }
+
+ // Repository information container
+ &__source {
+ display: none;
+ }
+
+ // [tablet -]: Layered navigation
+ @include break-to-device(tablet) {
+
+ // Primary and nested navigation
+ &--primary,
+ &--primary & {
+ position: absolute;
+ inset-inline: 0;
+ top: 0;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background-color: var(--md-default-bg-color);
+ }
+
+ // Primary navigation
+ &--primary {
+
+ // Navigation title and item
+ :is(.md-nav__title, .md-nav__item) {
+ font-size: px2rem(16px);
+ line-height: 1.5;
+ }
+
+ // Navigation title
+ .md-nav__title {
+ position: relative;
+ height: px2rem(112px);
+ padding: px2rem(60px) px2rem(16px) px2rem(4px);
+ line-height: px2rem(48px);
+ color: var(--md-default-fg-color--light);
+ white-space: nowrap;
+ cursor: pointer;
+ background-color: var(--md-default-fg-color--lightest);
+
+ // Navigation icon
+ .md-nav__icon {
+ position: absolute;
+ top: px2rem(8px);
+ inset-inline-start: px2rem(8px);
+ display: block;
+ width: px2rem(24px);
+ height: px2rem(24px);
+ margin: px2rem(4px);
+
+ // Navigation icon in link to previous level
+ &::after {
+ display: block;
+ width: 100%;
+ height: 100%;
+ content: "";
+ background-color: currentcolor;
+ mask-image: var(--md-nav-icon--prev);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+ }
+
+ // Navigation list
+ ~ .md-nav__list {
+ overflow-y: auto;
+ touch-action: pan-y;
+ background-color: var(--md-default-bg-color);
+ box-shadow:
+ 0 px2rem(1px) 0 var(--md-default-fg-color--lightest) inset;
+ scroll-snap-type: y mandatory;
+
+ // Omit border on first child
+ > :first-child {
+ border-top: 0;
+ }
+ }
+
+ // Top-level navigation title
+ &[for="__drawer"] {
+ font-weight: 700;
+ color: var(--md-primary-bg-color);
+ background-color: var(--md-primary-fg-color);
+ }
+
+ // Button with logo, pointing to `config.site_url`
+ .md-logo {
+ position: absolute;
+ inset-inline: px2rem(4px);
+ top: px2rem(4px);
+ display: block;
+ padding: px2rem(8px);
+ margin: px2rem(4px);
+ }
+ }
+
+ // Navigation list
+ .md-nav__list {
+ flex: 1;
+ }
+
+ // Navigation item
+ .md-nav__item {
+ border-top: px2rem(1px) solid var(--md-default-fg-color--lightest);
+
+ // Navigation link in active navigation
+ &--active > .md-nav__link {
+ color: var(--md-typeset-a-color);
+
+ // Navigation link on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+ }
+ }
+ }
+
+ // Navigation link
+ .md-nav__link {
+ padding: px2rem(12px) px2rem(16px);
+ margin-top: 0;
+
+ // Navigation link icon
+ svg {
+ margin-top: 0.1em;
+ }
+
+ // Adjust spacing on nested link
+ > .md-nav__link {
+ padding: 0;
+ }
+
+ // Navigation icon
+ .md-nav__icon {
+ width: px2rem(24px);
+ height: px2rem(24px);
+ margin-inline-end: px2rem(-4px);
+ font-size: px2rem(24px);
+
+ // Navigation icon in link to next level
+ &::after {
+ display: block;
+ width: 100%;
+ height: 100%;
+ content: "";
+ background-color: currentcolor;
+ mask-image: var(--md-nav-icon--next);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+ }
+ }
+
+ // Flip icon vertically
+ .md-nav__icon {
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] &::after {
+ transform: scale(-1);
+ }
+ }
+
+ // Table of contents contained in primary navigation
+ .md-nav--secondary {
+
+ // Navigation on level 2-6
+ .md-nav {
+ position: static;
+ background-color: transparent;
+
+ // Navigation link on level 3
+ .md-nav__link {
+ padding-inline-start: px2rem(28px);
+ }
+
+ // Navigation link on level 4
+ .md-nav .md-nav__link {
+ padding-inline-start: px2rem(40px);
+ }
+
+ // Navigation link on level 5
+ .md-nav .md-nav .md-nav__link {
+ padding-inline-start: px2rem(52px);
+ }
+
+ // Navigation link on level 6
+ .md-nav .md-nav .md-nav .md-nav__link {
+ padding-inline-start: px2rem(64px);
+ }
+ }
+ }
+ }
+
+ // Table of contents
+ &--secondary {
+ background-color: transparent;
+ }
+
+ // Hide nested navigation
+ &__toggle ~ & {
+ display: flex;
+ opacity: 0;
+ transition:
+ transform 250ms cubic-bezier(0.8, 0, 0.6, 1),
+ opacity 125ms 50ms;
+ transform: translateX(100%);
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: translateX(-100%);
+ }
+ }
+
+ // Show nested navigation when toggle is active
+ &__toggle:checked ~ & {
+ opacity: 1;
+ transition:
+ transform 250ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 125ms 125ms;
+ transform: translateX(0);
+
+ // Navigation list
+ > .md-nav__list {
+ // Hack: promote to own layer to reduce jitter
+ backface-visibility: hidden;
+ }
+ }
+ }
+
+ // [tablet portrait -]: Layered navigation with table of contents
+ @include break-to-device(tablet portrait) {
+
+ // Show link to table of contents
+ &--primary &__link[for="__toc"] {
+ display: flex;
+
+ // Show table of contents icon
+ .md-icon::after {
+ content: "";
+ }
+
+ // Hide navigation link to current page
+ + .md-nav__link {
+ display: none;
+ }
+
+ // Show table of contents
+ ~ .md-nav {
+ display: flex;
+ }
+ }
+
+ // Repository information container
+ &__source {
+ display: block;
+ padding: 0 px2rem(4px);
+ color: var(--md-primary-bg-color);
+ background-color: var(--md-primary-fg-color--dark);
+ }
+ }
+
+ // [tablet landscape]: Layered navigation with table of contents
+ @include break-at-device(tablet landscape) {
+
+ // Show link to integrated table of contents
+ &--integrated &__link[for="__toc"] {
+ display: flex;
+
+ // Show table of contents icon
+ .md-icon::after {
+ content: "";
+ }
+
+ // Hide navigation link to current page
+ + .md-nav__link {
+ display: none;
+ }
+
+ // Show table of contents
+ ~ .md-nav {
+ display: flex;
+ }
+ }
+ }
+
+ // [tablet landscape +]: Tree-like table of contents
+ @include break-from-device(tablet landscape) {
+ margin-bottom: px2rem(-8px);
+
+ // Table of contents
+ &--secondary {
+
+ // Navigation title
+ .md-nav__title {
+ position: sticky;
+ top: 0;
+ // Hack: because of the hack that we need to make .md-ellipsis work in
+ // Safari, we need to set `z-index` here as - see https://bit.ly/3s5M2jm
+ z-index: 1;
+ background: var(--md-default-bg-color);
+ box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color);
+
+ // Adjust snapping behavior
+ &[for="__toc"] {
+ scroll-snap-align: start;
+ }
+
+ // Hide navigation icon
+ .md-nav__icon {
+ display: none;
+ }
+ }
+
+ // Adjust spacing for navigation list - same reason as below
+ .md-nav__list {
+ padding-inline-start: px2rem(12px);
+ padding-bottom: px2rem(8px);
+ }
+
+ // Adjust spacing for navigation link - before this change, we set spacing
+ // on the left and right of a navigation item, but this led to the problem
+ // of cropped focus outlines, because we must set `overflow: hidden` on
+ // the navigation list for smooth expand and collapse transitions.
+ .md-nav__item > .md-nav__link {
+ margin-inline-end: px2rem(8px);
+ }
+ }
+ }
+
+ // [screen +]: Tree-like navigation
+ @include break-from-device(screen) {
+ margin-bottom: px2rem(-8px);
+ transition: max-height 250ms cubic-bezier(0.86, 0, 0.07, 1);
+
+ // Primary navigation
+ &--primary {
+
+ // Navigation title
+ .md-nav__title {
+ position: sticky;
+ top: 0;
+ // Hack: because of the hack that we need to make .md-ellipsis work in
+ // Safari, we need to set `z-index` here as - see https://bit.ly/3s5M2jm
+ z-index: 1;
+ background: var(--md-default-bg-color);
+ box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color);
+
+ // Adjust snapping behavior
+ &[for="__drawer"] {
+ scroll-snap-align: start;
+ }
+
+ // Hide navigation icon
+ .md-nav__icon {
+ display: none;
+ }
+ }
+
+ // Adjust spacing for navigation list - same reason as below
+ .md-nav__list {
+ padding-inline-start: px2rem(12px);
+ padding-bottom: px2rem(8px);
+ }
+
+ // Adjust spacing for navigation link - before this change, we set spacing
+ // on the left and right of a navigation item, but this led to the problem
+ // of cropped focus outlines, because we must set `overflow: hidden` on
+ // the navigation list for smooth expand and collapse transitions.
+ .md-nav__item > .md-nav__link {
+ margin-inline-end: px2rem(8px);
+ }
+ }
+
+ // Hide nested navigation
+ &__toggle ~ & {
+ display: grid;
+ grid-template-rows: 0fr;
+ visibility: collapse;
+ opacity: 0;
+ transition:
+ grid-template-rows 250ms cubic-bezier(0.86, 0, 0.07, 1),
+ opacity 250ms,
+ visibility 0ms 250ms;
+
+ // Navigation list
+ > .md-nav__list {
+ overflow: hidden;
+ }
+ }
+
+ // Show nested navigation when toggle is active or indeterminate
+ &__toggle:is(:checked, :indeterminate) ~ & {
+ grid-template-rows: 1fr;
+ visibility: visible;
+ opacity: 1;
+ transition:
+ grid-template-rows 250ms cubic-bezier(0.86, 0, 0.07, 1),
+ opacity 150ms 100ms,
+ visibility 0ms;
+ }
+
+ // Hide navigation title in nested navigation
+ &__item--nested > & > &__title {
+ display: none;
+ }
+
+ // Navigation section
+ &__item--section {
+ display: block;
+ margin: 1.25em 0;
+
+ // Adjust spacing on last child
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ // Show navigation link as title
+ > .md-nav__link {
+ font-weight: 700;
+
+ // Make labels discernable from links
+ &[for] {
+ color: var(--md-default-fg-color--light);
+ }
+
+ // Omit clicks if not a section index page
+ &:not(.md-nav__container) {
+ pointer-events: none;
+ }
+
+ // Hide navigation icon
+ > [for],
+ .md-icon {
+ display: none;
+ }
+ }
+
+ // Navigation
+ > .md-nav {
+ display: block;
+ margin-inline-start: px2rem(-12px);
+ visibility: visible;
+ opacity: 1;
+
+ // Adjust spacing on next level item
+ > .md-nav__list > .md-nav__item {
+ padding: 0;
+ }
+ }
+ }
+
+ // Navigation icon
+ &__icon {
+ width: px2rem(18px);
+ height: px2rem(18px);
+ border-radius: 100%;
+ transition: background-color 250ms;
+
+ // Navigation icon on hover
+ &:hover {
+ background-color: var(--md-accent-fg-color--transparent);
+ }
+
+ // Navigation icon content
+ &::after {
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ vertical-align: px2rem(-2px);
+ content: "";
+ background-color: currentcolor;
+ border-radius: 100%;
+ transition: transform 250ms;
+ mask-image: var(--md-nav-icon--next);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: rotate(180deg);
+ }
+
+ // Navigation icon - rotate icon when toggle is active or indeterminate
+ .md-nav__item--nested .md-nav__toggle:checked ~ .md-nav__link &,
+ .md-nav__item--nested .md-nav__toggle:indeterminate ~ .md-nav__link & {
+ transform: rotate(90deg);
+ }
+ }
+ }
+
+ // Modifier for when navigation tabs are rendered
+ &--lifted {
+
+ // Hide site title
+ > .md-nav__title {
+ display: none;
+ }
+
+ // Hide level 0 navigation items
+ > .md-nav__list > .md-nav__item {
+ display: none;
+
+ // Active parent navigation item
+ &--active {
+ display: block;
+
+ // Show navigation link as title
+ > .md-nav__link {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ margin-top: 0;
+ background: var(--md-default-bg-color);
+ box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color);
+
+ // Omit clicks if not a section index page
+ &:not(.md-nav__container) {
+ pointer-events: none;
+ }
+ }
+
+ // Adjust spacing for navigation section
+ &.md-nav__item--section {
+ margin: 0;
+ }
+ }
+
+ // Adjust spacing for nested navigation
+ > .md-nav {
+ margin-inline-start: px2rem(-12px);
+ }
+
+ // Make labels discernable from links
+ > [for] {
+ color: var(--md-default-fg-color--light);
+ }
+ }
+
+ // Hack: Always show active navigation tab on breakpoint screen, despite
+ // of checkbox being checked or not - see https://t.ly/Qc311
+ .md-nav[data-md-level="1"] {
+ grid-template-rows: 1fr;
+ visibility: visible;
+ opacity: 1;
+ }
+ }
+
+ // Modifier for when table of contents is rendered in primary navigation
+ &--integrated > .md-nav__list > .md-nav__item--active {
+
+ // Add spacing to container for non-nested navigation items
+ &:not(.md-nav__item--nested) {
+ padding: 0 px2rem(12px);
+
+ // Remove padding as it's given by container
+ > .md-nav__link {
+ padding: 0;
+ }
+ }
+
+ // Show integrated table of contents
+ .md-nav--secondary {
+ display: block;
+ margin-bottom: 1.25em;
+ visibility: visible;
+ border-inline-start: px2rem(1px) solid var(--md-primary-fg-color);
+ opacity: 1;
+
+ // Navigation list
+ > .md-nav__list {
+ padding-bottom: 0;
+ overflow: visible;
+ }
+
+ // Hide table of contents title
+ > .md-nav__title {
+ display: none;
+ }
+ }
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_pagination.scss b/src/templates/assets/stylesheets/main/components/_pagination.scss
new file mode 100644
index 00000000..a010bf43
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_pagination.scss
@@ -0,0 +1,85 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Pagination
+.md-pagination {
+ display: flex;
+ gap: px2rem(8px);
+ align-items: center;
+ justify-content: center;
+ font-size: px2rem(16px);
+ font-weight: 700;
+
+ // Pagination item
+ > * {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: px2rem(36px);
+ height: px2rem(36px);
+ text-align: center;
+ border-radius: px2rem(4px);
+ }
+
+ // Active pagination item
+ &__current {
+ color: var(--md-default-fg-color--light);
+ background-color: var(--md-default-fg-color--lightest);
+ }
+
+ // Pagination link
+ &__link {
+ transition:
+ color 125ms,
+ background-color 125ms;
+
+ // Pagination link on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+ background-color: var(--md-accent-fg-color--transparent);
+
+ // Pagination icon
+ svg {
+ color: var(--md-accent-fg-color);
+ }
+ }
+
+ // Show outline for keyboard devices
+ &.focus-visible {
+ outline-color: var(--md-accent-fg-color);
+ outline-offset: px2rem(4px);
+ }
+
+ // Pagination icon
+ svg {
+ display: block;
+ width: px2rem(24px);
+ max-height: 100%;
+ color: var(--md-default-fg-color--lighter);
+ fill: currentcolor;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_post.scss b/src/templates/assets/stylesheets/main/components/_post.scss
new file mode 100644
index 00000000..cf6ce019
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_post.scss
@@ -0,0 +1,196 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Post
+.md-post {
+
+ // Post backlink
+ &__back {
+ padding-bottom: px2rem(24px);
+ margin-bottom: px2rem(24px);
+ border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest);
+
+ // [tablet -]: Hide post backlink
+ @include break-to-device(tablet) {
+ display: none;
+ }
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+
+ // Flip icon vertically
+ svg {
+ transform: scaleX(-1);
+ }
+ }
+ }
+
+ // Post authors
+ &__authors {
+ display: flex;
+ flex-direction: column;
+ gap: px2rem(12px);
+ margin: 0 px2rem(12px) px2rem(24px);
+ }
+
+ // Post metadata
+ .md-post__meta {
+
+ // Navigation link
+ a {
+ transition: color 125ms;
+
+ // Navigation link on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+ }
+ }
+ }
+
+ // Post navigation title @todo - generalize
+ &__title {
+ font-weight: 700;
+ color: var(--md-default-fg-color--light);
+ }
+
+ // Post excerpt
+ &--excerpt {
+ margin-bottom: px2rem(64px);
+
+ // Post excerpt header
+ .md-post__header {
+ display: flex;
+ gap: px2rem(12px);
+ align-items: center;
+ min-height: px2rem(32px);
+ }
+
+ // Post excerpt authors
+ .md-post__authors {
+ display: inline-flex;
+ flex-direction: row;
+ gap: px2rem(4px);
+ align-items: center;
+ min-height: px2rem(48px);
+ margin: 0;
+ }
+
+ // Post excerpt metadata
+ .md-post__meta .md-meta__list {
+ margin-inline-end: px2rem(8px);
+ }
+
+ // Post excerpt content
+ .md-post__content > :first-child {
+ --md-scroll-margin: #{px2rem(120px)};
+
+ margin-top: 0;
+ }
+ }
+
+ // Add margin to table of contents
+ > .md-nav--secondary {
+ margin: 1em 0;
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+// Post author profile
+.md-profile {
+ display: flex;
+ gap: px2rem(12px);
+ align-items: center;
+ width: 100%;
+ font-size: px2rem(14px);
+ line-height: 1.4;
+
+ // Post author description
+ &__description {
+ flex-grow: 1;
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+// Content area for post
+.md-content--post {
+ display: flex;
+
+ // [tablet -]: Switch to inverted column layout
+ @include break-to-device(tablet) {
+ flex-flow: column-reverse;
+ }
+
+ // Content wrapper
+ > .md-content__inner {
+ min-width: 0;
+
+ // [screen +]: Adjust spacing between content area and sidebars
+ @include break-from-device(screen) {
+ margin-inline-start: px2rem(24px);
+ }
+ }
+}
+
+// Sidebar for post
+.md-sidebar.md-sidebar--post {
+
+ // [tablet -]: Adjust spacing
+ @include break-to-device(tablet) {
+ position: initial;
+ width: 100%;
+ padding: 0;
+
+ .md-sidebar__inner {
+ padding: 0;
+ }
+
+ .md-post__meta {
+ margin-inline: px2rem(12px);
+ }
+
+ .md-nav__item {
+ display: inline;
+ border: none;
+ }
+
+ .md-nav__list {
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: px2rem(12px);
+ padding-block: px2rem(12px);
+ }
+
+ .md-nav__link {
+ padding: 0;
+ }
+
+ .md-nav {
+ position: initial;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_progress.scss b/src/templates/assets/stylesheets/main/components/_progress.scss
new file mode 100644
index 00000000..7386ae33
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_progress.scss
@@ -0,0 +1,53 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Progress variables
+:root {
+ --md-progress-value: 0;
+ --md-progress-delay: 400ms;
+}
+
+// ----------------------------------------------------------------------------
+
+// Progress indicator
+.md-progress {
+ position: fixed;
+ top: 0;
+ z-index: 4;
+ width: 100%;
+ height: px2rem(1.5px);
+ background: var(--md-primary-bg-color);
+ opacity:
+ min(
+ clamp(0, var(--md-progress-value), 1),
+ clamp(0, 100 - var(--md-progress-value), 1)
+ );
+ transition:
+ transform 500ms cubic-bezier(0.19, 1, 0.22, 1),
+ opacity 250ms var(--md-progress-delay);
+ transform: scaleX(calc(var(--md-progress-value) * 1%));
+ transform-origin: left;
+}
diff --git a/src/templates/assets/stylesheets/main/components/_search.scss b/src/templates/assets/stylesheets/main/components/_search.scss
new file mode 100644
index 00000000..e0f36b0c
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_search.scss
@@ -0,0 +1,707 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Search variables
+:root {
+ --md-search-result-icon: svg-load("material/file-search-outline.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Search
+.md-search {
+ position: relative;
+
+ // [tablet landscape +]: Header-embedded search
+ @include break-from-device(tablet landscape) {
+ padding: px2rem(4px) 0;
+ }
+
+ // [no-js]: Hide search
+ .no-js & {
+ display: none;
+ }
+
+ // Search overlay
+ &__overlay {
+ z-index: 1;
+ opacity: 0;
+
+ // [tablet portrait -]: Search modal
+ @include break-to-device(tablet portrait) {
+ position: absolute;
+ top: px2rem(-20px);
+ width: px2rem(40px);
+ height: px2rem(40px);
+ overflow: hidden;
+ pointer-events: none;
+ background-color: var(--md-default-bg-color);
+ border-radius: px2rem(20px);
+ transition:
+ transform 300ms 100ms,
+ opacity 200ms 200ms;
+ transform-origin: center;
+ inset-inline-start: px2rem(-44px);
+
+ // Show overlay when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+ opacity: 1;
+ transition:
+ transform 400ms,
+ opacity 100ms;
+ }
+ }
+
+ // [tablet landscape +]: Header-embedded search
+ @include break-from-device(tablet landscape) {
+ position: fixed;
+ top: 0;
+ width: 0;
+ height: 0;
+ cursor: pointer;
+ background-color: hsla(0, 0%, 0%, 0.54);
+ transition:
+ width 0ms 250ms,
+ height 0ms 250ms,
+ opacity 250ms;
+ inset-inline-start: 0;
+
+ // Show overlay when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+ width: 100%;
+ // Hack: when the header is translated upon scrolling, a new layer is
+ // induced, which means that the height will now refer to the height of
+ // the header, albeit positioning is fixed. This should be mitigated
+ // in all cases when setting the height to 2x the viewport.
+ height: 200vh;
+ opacity: 1;
+ transition:
+ width 0ms,
+ height 0ms,
+ opacity 250ms;
+ }
+ }
+
+ // Adjust appearance when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+
+ // [mobile portrait -]: Scale up 45 times
+ @include break-to-device(mobile portrait) {
+ transform: scale(45);
+ }
+
+ // [mobile landscape]: Scale up 60 times
+ @include break-at-device(mobile landscape) {
+ transform: scale(60);
+ }
+
+ // [tablet portrait]: Scale up 75 times
+ @include break-at-device(tablet portrait) {
+ transform: scale(75);
+ }
+ }
+ }
+
+ // Search wrapper
+ &__inner {
+ // Hack: promote to own layer to reduce jitter
+ backface-visibility: hidden;
+
+ // [tablet portrait -]: Search modal
+ @include break-to-device(tablet portrait) {
+ position: fixed;
+ top: 0;
+ z-index: 2;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+ opacity: 0;
+ transition:
+ width 0ms 300ms,
+ height 0ms 300ms,
+ transform 150ms 150ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 150ms 150ms;
+ transform: translateX(5%);
+ inset-inline-start: 0;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: translateX(-5%);
+ }
+
+ // Adjust appearance when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+ width: 100%;
+ height: 100%;
+ opacity: 1;
+ transition:
+ width 0ms 0ms,
+ height 0ms 0ms,
+ transform 150ms 150ms cubic-bezier(0.1, 0.7, 0.1, 1),
+ opacity 150ms 150ms;
+ transform: translateX(0);
+ }
+ }
+
+ // [tablet landscape +]: Header-embedded search
+ @include break-from-device(tablet landscape) {
+ position: relative;
+ float: inline-end;
+ width: px2rem(234px);
+ padding: px2rem(2px) 0;
+ transition: width 250ms cubic-bezier(0.1, 0.7, 0.1, 1);
+ }
+
+ // Adjust appearance when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+
+ // [tablet landscape]: Omit overlaying header title
+ @include break-at-device(tablet landscape) {
+ width: px2rem(468px);
+ }
+
+ // [screen +]: Match width of content area
+ @include break-from-device(screen) {
+ width: px2rem(688px);
+ }
+ }
+ }
+
+ // Search form
+ &__form {
+ position: relative;
+ z-index: 2;
+ height: px2rem(48px);
+ background-color: var(--md-default-bg-color);
+ box-shadow: 0 0 px2rem(12px) transparent;
+ transition:
+ color 250ms,
+ background-color 250ms;
+
+ // [tablet landscape +]: Header-embedded search
+ @include break-from-device(tablet landscape) {
+ height: px2rem(36px);
+ background-color: hsla(0, 0%, 0%, 0.26);
+ border-radius: px2rem(2px);
+
+ // Search form on hover
+ &:hover {
+ background-color: hsla(0, 0%, 100%, 0.12);
+ }
+ }
+
+ // Adjust appearance when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+ color: var(--md-default-fg-color);
+ background-color: var(--md-default-bg-color);
+ border-radius: px2rem(2px) px2rem(2px) 0 0;
+ box-shadow: 0 0 px2rem(12px) hsla(0, 0%, 0%, 0.07);
+ }
+ }
+
+ // Search input
+ &__input {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ height: 100%;
+ padding-inline: px2rem(72px) px2rem(44px);
+ font-size: px2rem(18px);
+ text-overflow: ellipsis;
+ background: transparent;
+
+ // Search placeholder
+ &::placeholder {
+ transition: color 250ms;
+ }
+
+ // Search icon and placeholder
+ ~ .md-search__icon,
+ &::placeholder {
+ color: var(--md-default-fg-color--light);
+ }
+
+ // Remove the "x" rendered by Internet Explorer
+ &::-ms-clear {
+ display: none;
+ }
+
+ // [tablet portrait -]: Search modal
+ @include break-to-device(tablet portrait) {
+ width: 100%;
+ height: px2rem(48px);
+ font-size: px2rem(18px);
+ }
+
+ // [tablet landscape +]: Header-embedded search
+ @include break-from-device(tablet landscape) {
+ padding-inline-start: px2rem(44px);
+ font-size: px2rem(16px);
+ color: inherit;
+
+ // Search placeholder
+ &::placeholder {
+ color: var(--md-primary-bg-color--light);
+ }
+
+ // Search icon
+ + .md-search__icon {
+ color: var(--md-primary-bg-color);
+ }
+
+ // Adjust appearance when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+ text-overflow: clip;
+
+ // Search icon and placeholder
+ + .md-search__icon {
+ color: var(--md-default-fg-color--light);
+ }
+
+ // Search placeholder
+ &::placeholder {
+ color: transparent;
+ }
+ }
+ }
+ }
+
+ // Search icon
+ &__icon {
+ display: inline-block;
+ width: px2rem(24px);
+ height: px2rem(24px);
+ cursor: pointer;
+ transition:
+ color 250ms,
+ opacity 250ms;
+
+ // Search icon on hover
+ &:hover {
+ opacity: 0.7;
+ }
+
+ // Search focus button
+ &[for="__search"] {
+ position: absolute;
+ top: px2rem(6px);
+ inset-inline-start: px2rem(10px);
+ z-index: 2;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & svg {
+ transform: scaleX(-1);
+ }
+
+ // [tablet portrait -]: Search modal
+ @include break-to-device(tablet portrait) {
+ top: px2rem(12px);
+ inset-inline-start: px2rem(16px);
+
+ // Hide the magnifying glass
+ svg:first-child {
+ display: none;
+ }
+ }
+
+ // [tablet landscape +]: Header-embedded search
+ @include break-from-device(tablet landscape) {
+ pointer-events: none;
+
+ // Hide the back arrow
+ svg:last-child {
+ display: none;
+ }
+ }
+ }
+ }
+
+ // Search options
+ &__options {
+ position: absolute;
+ top: px2rem(6px);
+ inset-inline-end: px2rem(10px);
+ z-index: 2;
+ pointer-events: none;
+
+ // [tablet portrait -]: Search modal
+ @include break-to-device(tablet portrait) {
+ top: px2rem(12px);
+ inset-inline-end: px2rem(16px);
+ }
+
+ // Search option buttons
+ > .md-icon {
+ margin-inline-start: px2rem(4px);
+ color: var(--md-default-fg-color--light);
+ opacity: 0;
+ transition:
+ transform 150ms cubic-bezier(0.1, 0.7, 0.1, 1),
+ opacity 150ms;
+ transform: scale(0.75);
+
+ // Hide outline for pointer devices
+ &:not(.focus-visible) {
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ // Show buttons when search is active and input non-empty
+ [data-md-toggle="search"]:checked ~ .md-header // stylelint-disable-line
+ .md-search__input:valid ~ & {
+ pointer-events: initial;
+ opacity: 1;
+ transform: scale(1);
+
+ // Search focus icon
+ &:hover {
+ opacity: 0.7;
+ }
+ }
+ }
+ }
+
+ // Search suggestions
+ &__suggest {
+ position: absolute;
+ top: 0;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ padding-inline: px2rem(72px) px2rem(44px);
+ font-size: px2rem(18px);
+ color: var(--md-default-fg-color--lighter);
+ white-space: nowrap;
+ opacity: 0;
+ transition: opacity 50ms;
+
+ // [tablet landscape +]: Header-embedded search
+ @include break-from-device(tablet landscape) {
+ padding-inline-start: px2rem(44px);
+ font-size: px2rem(16px);
+ }
+
+ // Show suggestions when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+ opacity: 1;
+ transition: opacity 300ms 100ms;
+ }
+ }
+
+ // Search output
+ &__output {
+ position: absolute;
+ z-index: 1;
+ width: 100%;
+ overflow: hidden;
+ border-end-start-radius: px2rem(2px);
+ border-end-end-radius: px2rem(2px);
+
+ // [tablet portrait -]: Search modal
+ @include break-to-device(tablet portrait) {
+ top: px2rem(48px);
+ bottom: 0;
+ }
+
+ // [tablet landscape +]: Header-embedded search
+ @include break-from-device(tablet landscape) {
+ top: px2rem(38px);
+ opacity: 0;
+ transition: opacity 400ms;
+
+ // Show output when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+ box-shadow: var(--md-shadow-z3);
+ opacity: 1;
+ }
+ }
+ }
+
+ // Search scroll wrapper
+ &__scrollwrap {
+ height: 100%;
+ overflow-y: auto;
+ // Hack: Chrome 88+ has weird overscroll behavior. Overall, scroll snapping
+ // seems to be something that is not ready for prime time on some browsers.
+ // scroll-snap-type: y mandatory;
+ touch-action: pan-y;
+ background-color: var(--md-default-bg-color);
+ // Hack: promote to own layer to reduce jitter
+ backface-visibility: hidden;
+
+ // Mitigiate excessive repaints on non-retina devices
+ @media (max-resolution: 1dppx) {
+ transform: translateZ(0);
+ }
+
+ // [tablet landscape]: Set fixed width to omit unnecessary reflow
+ @include break-at-device(tablet landscape) {
+ width: px2rem(468px);
+ }
+
+ // [screen +]: Set fixed width to omit unnecessary reflow
+ @include break-from-device(screen) {
+ width: px2rem(688px);
+ }
+
+ // [tablet landscape +]: Limit height to viewport
+ @include break-from-device(tablet landscape) {
+ max-height: 0;
+ scrollbar-width: thin;
+ scrollbar-color: var(--md-default-fg-color--lighter) transparent;
+
+ // Show scroll wrapper when search is active
+ [data-md-toggle="search"]:checked ~ .md-header & {
+ max-height: 75vh;
+ }
+
+ // Search scroll wrapper on hover
+ &:hover {
+ scrollbar-color: var(--md-accent-fg-color) transparent;
+ }
+
+ // Webkit scrollbar
+ &::-webkit-scrollbar {
+ width: px2rem(4px);
+ height: px2rem(4px);
+ }
+
+ // Webkit scrollbar thumb
+ &::-webkit-scrollbar-thumb {
+ background-color: var(--md-default-fg-color--lighter);
+
+ // Webkit scrollbar thumb on hover
+ &:hover {
+ background-color: var(--md-accent-fg-color);
+ }
+ }
+ }
+ }
+}
+
+// Search result
+.md-search-result {
+ color: var(--md-default-fg-color);
+ word-break: break-word;
+
+ // Search result metadata
+ &__meta {
+ padding: 0 px2rem(16px);
+ font-size: px2rem(12.8px);
+ line-height: px2rem(36px);
+ color: var(--md-default-fg-color--light);
+ background-color: var(--md-default-fg-color--lightest);
+ scroll-snap-align: start;
+
+ // [tablet landscape +]: Adjust spacing
+ @include break-from-device(tablet landscape) {
+ padding-inline-start: px2rem(44px);
+ }
+ }
+
+ // Search result list
+ &__list {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ // Hack: omit accidental text selection on fast toggle of more button
+ user-select: none;
+ }
+
+ // Search result item
+ &__item {
+ box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest);
+
+ // Omit border on first child
+ &:first-child {
+ box-shadow: none;
+ }
+ }
+
+ // Search result link
+ &__link {
+ display: block;
+ outline: none;
+ transition: background-color 250ms;
+ scroll-snap-align: start;
+
+ // Search result link on focus/hover
+ &:is(:focus, :hover) {
+ background-color: var(--md-accent-fg-color--transparent);
+ }
+
+ // Adjust spacing on last child of last link
+ &:last-child p:last-child {
+ margin-bottom: px2rem(12px);
+ }
+ }
+
+ // Search result more container
+ &__more > summary {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ display: block;
+ cursor: pointer;
+ outline: none;
+ scroll-snap-align: start;
+
+ // Hide native details marker
+ &::marker {
+ display: none;
+ }
+
+ // Hide native details marker - legacy, must be split into a seprate rule,
+ // so older browsers don't consider the selector list as invalid
+ &::-webkit-details-marker {
+ display: none;
+ }
+
+ // Search result more button
+ > div {
+ padding: px2em(12px) px2rem(16px);
+ font-size: px2rem(12.8px);
+ color: var(--md-typeset-a-color);
+ transition:
+ color 250ms,
+ background-color 250ms;
+
+ // [tablet landscape +]: Adjust spacing
+ @include break-from-device(tablet landscape) {
+ padding-inline-start: px2rem(44px);
+ }
+ }
+
+ // Search result more link on focus/hover
+ &:is(:focus, :hover) > div {
+ color: var(--md-accent-fg-color);
+ background-color: var(--md-accent-fg-color--transparent);
+ }
+ }
+
+ // Adjust background for more container in open state
+ &__more[open] > summary {
+ background-color: var(--md-default-bg-color);
+ // box-shadow: 0 px2rem(-1px) hsla(0, 0%, 0%, 0.07) inset;
+ }
+
+ // Search result article
+ &__article {
+ position: relative;
+ padding: 0 px2rem(16px);
+ overflow: hidden;
+
+ // [tablet landscape +]: Adjust spacing
+ @include break-from-device(tablet landscape) {
+ padding-inline-start: px2rem(44px);
+ }
+ }
+
+ // Search result icon
+ &__icon {
+ position: absolute;
+ inset-inline-start: 0;
+ width: px2rem(24px);
+ height: px2rem(24px);
+ margin: px2rem(10px);
+ color: var(--md-default-fg-color--light);
+
+ // [tablet portrait -]: Hide icon
+ @include break-to-device(tablet portrait) {
+ display: none;
+ }
+
+ // Search result icon content
+ &::after {
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ content: "";
+ background-color: currentcolor;
+ mask-image: var(--md-search-result-icon);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: scaleX(-1);
+ }
+ }
+ }
+
+ // Typesetted content
+ .md-typeset {
+ font-size: px2rem(12.8px);
+ line-height: 1.6;
+ color: var(--md-default-fg-color--light);
+
+ // Search result article title
+ h1 {
+ margin: px2rem(11px) 0;
+ font-size: px2rem(16px);
+ font-weight: 400;
+ line-height: 1.4;
+ color: var(--md-default-fg-color);
+
+ // Search term highlighting
+ mark {
+ text-decoration: none;
+ }
+ }
+
+ // Search result section title
+ h2 {
+ margin: 0.5em 0;
+ font-size: px2rem(12.8px);
+ font-weight: 700;
+ line-height: 1.6;
+ color: var(--md-default-fg-color);
+
+ // Search term highlighting
+ mark {
+ text-decoration: none;
+ }
+ }
+ }
+
+ // Search result terms
+ &__terms {
+ display: block;
+ margin: 0.5em 0;
+ font-size: px2rem(12.8px);
+ font-style: italic;
+ color: var(--md-default-fg-color);
+ }
+
+ // Search term highlighting
+ mark {
+ color: var(--md-accent-fg-color);
+ text-decoration: underline;
+ background-color: transparent;
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_select.scss b/src/templates/assets/stylesheets/main/components/_select.scss
new file mode 100644
index 00000000..ed597a39
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_select.scss
@@ -0,0 +1,115 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Selection
+.md-select {
+ position: relative;
+ z-index: 1;
+
+ // Selection tooltip
+ &__inner {
+ position: absolute;
+ top: calc(100% - #{px2rem(4px)});
+ left: 50%;
+ max-height: 0;
+ margin-top: px2rem(4px);
+ color: var(--md-default-fg-color);
+ background-color: var(--md-default-bg-color);
+ border-radius: px2rem(2px);
+ box-shadow: var(--md-shadow-z2);
+ opacity: 0;
+ transition:
+ transform 250ms 375ms,
+ opacity 250ms 250ms,
+ max-height 0ms 500ms;
+ transform: translate3d(-50%, px2rem(6px), 0);
+
+ // Selection bubble on parent focus/hover
+ .md-select:is(:focus-within, :hover) & {
+ max-height: px2rem(200px);
+ opacity: 1;
+ transition:
+ transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1),
+ opacity 250ms,
+ max-height 0ms;
+ transform: translate3d(-50%, 0, 0);
+ }
+
+ // Selection bubble handle
+ &::after {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ width: 0;
+ height: 0;
+ margin-top: px2rem(-4px);
+ margin-left: px2rem(-4px);
+ content: "";
+ border: px2rem(4px) solid transparent;
+ border-top: 0;
+ border-bottom-color: var(--md-default-bg-color);
+ }
+ }
+
+ // Selection list
+ &__list {
+ max-height: inherit;
+ padding: 0;
+ margin: 0;
+ overflow: auto;
+ font-size: px2rem(16px);
+ list-style-type: none;
+ border-radius: px2rem(2px);
+ }
+
+ // Selection item
+ &__item {
+ line-height: px2rem(36px);
+ }
+
+ // Selection link
+ &__link {
+ display: block;
+ width: 100%;
+ padding-inline: px2rem(12px) px2rem(24px);
+ cursor: pointer;
+ outline: none;
+ transition:
+ background-color 250ms,
+ color 250ms;
+ scroll-snap-align: start;
+
+ // Link on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+ }
+
+ // Link on focus
+ &:focus {
+ background-color: var(--md-default-fg-color--lightest);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_sidebar.scss b/src/templates/assets/stylesheets/main/components/_sidebar.scss
new file mode 100644
index 00000000..8a320c04
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_sidebar.scss
@@ -0,0 +1,209 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Sidebar
+.md-sidebar {
+ position: sticky;
+ top: px2rem(48px);
+ flex-shrink: 0;
+ align-self: flex-start;
+ width: px2rem(242px);
+ padding: px2rem(24px) 0;
+
+ // [print]: Hide sidebar
+ @media print {
+ display: none;
+ }
+
+ // Primary sidebar with navigation
+ &--primary {
+
+ // [tablet -]: Show navigation as drawer
+ @include break-to-device(tablet) {
+ position: fixed;
+ top: 0;
+ z-index: 5;
+ display: block;
+ width: px2rem(242px);
+ height: 100%;
+ background-color: var(--md-default-bg-color);
+ transition:
+ transform 250ms cubic-bezier(0.4, 0, 0.2, 1),
+ box-shadow 250ms;
+ transform: translateX(0);
+ inset-inline-start: px2rem(-242px);
+
+ // Show sidebar when drawer is active
+ [data-md-toggle="drawer"]:checked ~ .md-container & {
+ box-shadow: var(--md-shadow-z3);
+ transform: translateX(px2rem(242px));
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: translateX(px2rem(-242px));
+ }
+ }
+
+ // Stretch scroll wrapper for primary sidebar
+ .md-sidebar__scrollwrap {
+ position: absolute;
+ inset: 0;
+ margin: 0;
+ scroll-snap-type: none;
+ overflow: hidden;
+ }
+ }
+ }
+
+ // [screen +]: Show navigation as sidebar
+ @include break-from-device(screen) {
+ height: 0;
+
+ // [no-js]: Switch to native sticky behavior
+ .no-js & {
+ height: auto;
+ }
+
+ // Adjust spacing for sticky navigation tabs
+ .md-header--lifted ~ .md-container & {
+ top: px2rem(96px);
+ }
+ }
+
+ // Secondary sidebar with table of contents
+ &--secondary {
+ display: none;
+ order: 2;
+
+ // [tablet landscape +]: Show table of contents as sidebar
+ @include break-from-device(tablet landscape) {
+ height: 0;
+
+ // [no-js]: Switch to native sticky behavior
+ .no-js & {
+ height: auto;
+ }
+
+ // Sidebar is visible
+ &:not([hidden]) {
+ display: block;
+ }
+
+ // Ensure smooth scrolling on iOS
+ .md-sidebar__scrollwrap {
+ touch-action: pan-y;
+ }
+ }
+ }
+
+ // Sidebar scroll wrapper
+ &__scrollwrap {
+ margin: 0 px2rem(4px);
+ overflow-y: auto;
+ // Hack: promote to own layer to reduce jitter
+ backface-visibility: hidden;
+ // Hack: Chrome 81+ exhibits a strange bug, where it scrolls the container
+ // to the bottom if `scroll-snap-type` is set on the initial render. For
+ // this reason, we disable scroll snapping until this is resolved (#1667).
+ // scroll-snap-type: y mandatory;
+ scrollbar-width: thin;
+ scrollbar-gutter: stable;
+ scrollbar-color: var(--md-default-fg-color--lighter) transparent;
+
+ // Webkit scrollbar
+ &::-webkit-scrollbar {
+ width: px2rem(4px);
+ height: px2rem(4px);
+ }
+
+ // Sidebar scroll wrapper on focus/hover
+ &:is(:focus-within, :hover) {
+ scrollbar-color: var(--md-accent-fg-color) transparent;
+
+ // Webkit scrollbar thumb
+ &::-webkit-scrollbar-thumb {
+ background-color: var(--md-default-fg-color--lighter);
+
+ // Webkit scrollbar thumb on hover
+ &:hover {
+ background-color: var(--md-accent-fg-color);
+ }
+ }
+ }
+ }
+
+ // Hack: the scrollbar is only visible when the sidebar's contents overflow,
+ // which is nice, but leads to the problem where the chevrons of expandable
+ // sections will jump by `4px` when the sidebar is shown. We wanted to fix
+ // this problem for so long, but haven't found a clean way of doing it.
+ // Until now. The following declaration is only applied to Webkit browsers
+ // (e.g. Chrome and Safari), which support styling of scrollbars. The trick
+ // is to add conditional padding on the side of the scrollbar only if the
+ // sidebar's content doesn't overflow. This hack is inspired and adapted
+ // from Ayke van Laëthem's year old trick – see https://bit.ly/3Sb1qql
+ @supports selector(::-webkit-scrollbar) {
+
+ // Sidebar scroll wrapper
+ &__scrollwrap {
+ scrollbar-gutter: auto;
+ }
+
+ // Sidebar wrapper
+ &__inner {
+ padding-inline-end: calc(100% - #{px2rem(230px)});
+ }
+ }
+}
+
+// [tablet -]: Show overlay on active drawer
+@include break-to-device(tablet) {
+
+ // Drawer overlay
+ .md-overlay {
+ position: fixed;
+ top: 0;
+ z-index: 5;
+ width: 0;
+ height: 0;
+ background-color: hsla(0, 0%, 0%, 0.54);
+ opacity: 0;
+ transition:
+ width 0ms 250ms,
+ height 0ms 250ms,
+ opacity 250ms;
+
+ // Show overlay when drawer is active
+ [data-md-toggle="drawer"]:checked ~ & {
+ width: 100%;
+ height: 100%;
+ opacity: 1;
+ transition:
+ width 0ms,
+ height 0ms,
+ opacity 250ms;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_source.scss b/src/templates/assets/stylesheets/main/components/_source.scss
new file mode 100644
index 00000000..a2b72009
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_source.scss
@@ -0,0 +1,182 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Keyframes
+// ----------------------------------------------------------------------------
+
+// Show repository facts
+@keyframes facts {
+ 0% {
+ height: 0;
+ }
+
+ 100% {
+ height: px2rem(13px);
+ }
+}
+
+// Show repository fact
+@keyframes fact {
+ 0% {
+ opacity: 0;
+ transform: translateY(100%);
+ }
+
+ 50% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0%);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Repository information variables
+:root {
+ --md-source-forks-icon: svg-load("octicons/repo-forked-16.svg");
+ --md-source-repositories-icon: svg-load("octicons/repo-16.svg");
+ --md-source-stars-icon: svg-load("octicons/star-16.svg");
+ --md-source-version-icon: svg-load("octicons/tag-16.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Repository information
+.md-source {
+ display: block;
+ font-size: px2rem(13px);
+ line-height: 1.2;
+ white-space: nowrap;
+ outline-color: var(--md-accent-fg-color);
+ // Hack: promote to own layer to reduce jitter
+ backface-visibility: hidden;
+ transition: opacity 250ms;
+
+ // Repository information on hover
+ &:hover {
+ opacity: 0.7;
+ }
+
+ // Repository icon
+ &__icon {
+ display: inline-block;
+ width: px2rem(40px);
+ height: px2rem(48px);
+ vertical-align: middle;
+
+ // Align with margin only (as opposed to normal button alignment)
+ svg {
+ margin-inline-start: px2rem(12px);
+ margin-top: px2rem(12px);
+ }
+
+ // Adjust spacing if icon is present
+ + .md-source__repository {
+ padding-inline-start: px2rem(40px);
+ margin-inline-start: px2rem(-40px);
+ }
+ }
+
+ // Repository name
+ &__repository {
+ display: inline-block;
+ max-width: calc(100% - #{px2rem(24px)});
+ margin-inline-start: px2rem(12px);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ }
+
+ // Repository facts
+ &__facts {
+ display: flex;
+ gap: px2rem(8px);
+ width: 100%;
+ padding: 0;
+ margin: px2rem(2px) 0 0;
+ overflow: hidden;
+ font-size: px2rem(11px);
+ list-style-type: none;
+ opacity: 0.75;
+
+ // Show after the data was loaded
+ .md-source__repository--active & {
+ animation: facts 250ms ease-in;
+ }
+ }
+
+ // Repository fact
+ &__fact {
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ // Show after the data was loaded
+ .md-source__repository--active & {
+ animation: fact 400ms ease-out;
+ }
+
+ // Repository fact icon
+ &::before {
+ display: inline-block;
+ width: px2rem(12px);
+ height: px2rem(12px);
+ margin-inline-end: px2rem(2px);
+ vertical-align: text-top;
+ content: "";
+ background-color: currentcolor;
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ // Adjust spacing for 2nd+ fact
+ &:nth-child(1n+2) {
+ flex-shrink: 0;
+ }
+
+ // Repository fact: version
+ &--version::before {
+ mask-image: var(--md-source-version-icon);
+ }
+
+ // Repository fact: stars
+ &--stars::before {
+ mask-image: var(--md-source-stars-icon);
+ }
+
+ // Repository fact: forks
+ &--forks::before {
+ mask-image: var(--md-source-forks-icon);
+ }
+
+ // Repository fact: repositories
+ &--repositories::before {
+ mask-image: var(--md-source-repositories-icon);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_status.scss b/src/templates/assets/stylesheets/main/components/_status.scss
new file mode 100644
index 00000000..9e096021
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_status.scss
@@ -0,0 +1,73 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Status variables
+:root {
+ --md-status: svg-load("material/information-outline.svg");
+ --md-status--new: svg-load("material/alert-decagram.svg");
+ --md-status--deprecated: svg-load("material/trash-can.svg");
+ --md-status--encrypted: svg-load("material/shield-lock.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Status
+.md-status {
+
+ // Status icon
+ &::after {
+ display: inline-block;
+ width: px2em(18px);
+ height: px2em(18px);
+ vertical-align: text-bottom;
+ content: "";
+ background-color: var(--md-default-fg-color--light);
+ mask-image: var(--md-status);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ // Status icon on hover
+ &:hover::after {
+ background-color: currentcolor;
+ }
+
+ // Status: new
+ &--new::after {
+ mask-image: var(--md-status--new);
+ }
+
+ // Status: deprecated
+ &--deprecated::after {
+ mask-image: var(--md-status--deprecated);
+ }
+
+ // Status: encrypted
+ &--encrypted::after {
+ mask-image: var(--md-status--encrypted);
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_tabs.scss b/src/templates/assets/stylesheets/main/components/_tabs.scss
new file mode 100644
index 00000000..0da3384b
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_tabs.scss
@@ -0,0 +1,133 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Navigation tabs
+.md-tabs {
+ // Must be higher than the z-index of the back-to-top button, or the button
+ // will overlay the navigation tabs bar when scrolling up fast.
+ z-index: 3;
+ display: block;
+ width: 100%;
+ overflow: auto;
+ line-height: 1.3;
+ color: var(--md-primary-bg-color);
+ background-color: var(--md-primary-fg-color);
+
+ // [print]: Hide tabs
+ @media print {
+ display: none;
+ }
+
+ // [tablet -]: Hide tabs
+ @include break-to-device(tablet) {
+ display: none;
+ }
+
+ // Navigation tabs are hidden
+ &[hidden] {
+ pointer-events: none;
+ }
+
+ // Navigation tabs list
+ &__list {
+ display: flex;
+ padding: 0;
+ margin: 0;
+ margin-inline-start: px2rem(4px);
+ overflow: auto;
+ white-space: nowrap;
+ list-style: none;
+ contain: content;
+ // Hack: don't show scrollbar when navigation tabs overflow, which should
+ // only happen in rare occasions, as adding too many top level sections is
+ // discouraged, since hiding content on horitontal axis doesn't lead to a
+ // good user experience. It's just harder to discover.
+ scrollbar-width: none;
+
+ // Hack: see above
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ // Navigation tabs item
+ &__item {
+ height: px2rem(48px);
+ padding-inline: px2rem(12px);
+
+ // Navigation tabs link in active navigation
+ &--active .md-tabs__link {
+ color: inherit;
+ opacity: 1;
+ }
+ }
+
+ // Navigation tabs link - could be defined as block elements and aligned via
+ // line height, but this would imply more repaints when scrolling
+ &__link {
+ display: flex;
+ margin-top: px2rem(16px);
+ font-size: px2rem(14px);
+ outline-color: var(--md-accent-fg-color);
+ outline-offset: px2rem(4px);
+ // Hack: save a repaint when tabs are appearing on scrolling up
+ backface-visibility: hidden;
+ opacity: 0.7;
+ transition:
+ transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),
+ opacity 250ms;
+
+ // Navigation tabs link on focus/hover
+ &:is(:focus, :hover) {
+ color: inherit;
+ opacity: 1;
+ }
+
+ // Navigation tabs link icon
+ svg {
+ height: 1.3em;
+ margin-inline-end: px2rem(8px);
+ fill: currentcolor;
+ }
+
+ // Delay transitions by a small amount
+ @for $i from 2 through 16 {
+ .md-tabs__item:nth-child(#{$i}) & {
+ transition-delay: 20ms * ($i - 1);
+ }
+ }
+
+ // Hide tabs upon scrolling - disable transition to minimizes repaints
+ // while scrolling down, while scrolling up seems to be okay
+ .md-tabs[hidden] & {
+ opacity: 0;
+ transition:
+ transform 0ms 100ms,
+ opacity 100ms;
+ transform: translateY(50%);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_tag.scss b/src/templates/assets/stylesheets/main/components/_tag.scss
new file mode 100644
index 00000000..9f31829d
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_tag.scss
@@ -0,0 +1,105 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Tag variables
+:root {
+ --md-tag-icon: svg-load("material/pound.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Tag list
+ .md-tags {
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: px2em(8px);
+ margin-top: px2em(-2px);
+ margin-bottom: px2em(12px);
+ }
+
+ // Tag
+ .md-tag {
+ display: inline-flex;
+ gap: px2em(8px);
+ align-items: center;
+ padding: px2em(4px, 12.8px) px2em(10px, 12.8px);
+ font-size: px2rem(12.8px); // Fallback
+ font-size: min(px2em(12.8px), px2rem(12.8px));
+ font-weight: 700;
+ line-height: 1.6;
+ letter-spacing: initial;
+ background: var(--md-default-fg-color--lightest);
+ border-radius: px2rem(48px);
+
+ // Linked tag
+ &[href] {
+ color: inherit;
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ transition:
+ color 125ms,
+ background-color 125ms;
+
+ // Linked tag on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-bg-color);
+ background-color: var(--md-accent-fg-color);
+ }
+ }
+
+ // Tag inside headline
+ [id] > & {
+ vertical-align: text-top;
+ }
+ }
+
+ // Tag icon
+ .md-tag-icon {
+
+ // Tag icon content
+ &::before {
+ display: inline-block;
+ width: 1.2em;
+ height: 1.2em;
+ vertical-align: text-bottom;
+ content: "";
+ background-color: var(--md-default-fg-color--lighter);
+ transition: background-color 125ms;
+ mask-image: var(--md-tag-icon);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ // Linked tag on focus/hover
+ &[href]:is(:focus, :hover)::before {
+ background-color: var(--md-accent-bg-color);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_tooltip.scss b/src/templates/assets/stylesheets/main/components/_tooltip.scss
new file mode 100644
index 00000000..421e5858
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_tooltip.scss
@@ -0,0 +1,292 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Keyframes
+// ----------------------------------------------------------------------------
+
+// Continuous pulse animation
+@keyframes pulse {
+ 0% {
+ transform: scale(0.95);
+ }
+
+ 75% {
+ transform: scale(1);
+ }
+
+ 100% {
+ transform: scale(0.95);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Tooltip variables
+:root {
+ --md-annotation-bg-icon: svg-load("material/circle.svg");
+ --md-annotation-icon: svg-load("material/plus-circle.svg");
+ --md-tooltip-width: #{px2rem(400px)};
+}
+
+// ----------------------------------------------------------------------------
+
+// Tooltip
+.md-tooltip {
+ position: absolute;
+ top: var(--md-tooltip-y);
+ left:
+ clamp(
+ var(--md-tooltip-0, #{px2rem(0px)}) + #{px2rem(16px)},
+ var(--md-tooltip-x),
+ 100vw +
+ var(--md-tooltip-0, #{px2rem(0px)}) + #{px2rem(16px)} -
+ var(--md-tooltip-width) -
+ 2 * #{px2rem(16px)}
+ );
+ // Hack: set an explicit `z-index` so we can transition it to ensure that any
+ // following elements are not overlaying the tooltip during the transition.
+ z-index: 0;
+ width: var(--md-tooltip-width);
+ max-width: calc(100vw - 2 * #{px2rem(16px)});
+ font-family: var(--md-text-font-family);
+ color: var(--md-default-fg-color);
+ background-color: var(--md-default-bg-color);
+ border-radius: px2rem(2px);
+ box-shadow: var(--md-shadow-z2);
+ opacity: 0;
+ transition:
+ transform 0ms 250ms,
+ opacity 250ms,
+ z-index 250ms;
+ transform: translateY(px2rem(-8px));
+ // Hack: promote to own layer to reduce jitter
+ backface-visibility: hidden;
+
+ // Active tooltip
+ &--active {
+ z-index: 2;
+ opacity: 1;
+ transition:
+ transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1),
+ opacity 250ms,
+ z-index 0ms;
+ transform: translateY(0);
+ }
+
+ // Show outline on target and for keyboard devices
+ :is(.focus-visible > &, &:target) {
+ outline: var(--md-accent-fg-color) auto;
+ }
+
+ // Tooltip wrapper
+ &__inner {
+ padding: px2rem(16px);
+ font-size: px2rem(12.8px);
+
+ // Adjust spacing on first child
+ &.md-typeset > :first-child {
+ margin-top: 0;
+ }
+
+ // Adjust spacing on last child
+ &.md-typeset > :last-child {
+ margin-bottom: 0;
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+// Annotation
+.md-annotation {
+ font-weight: 400;
+ white-space: normal;
+ vertical-align: text-bottom;
+ outline: none;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ direction: rtl;
+ }
+
+ // Annotation index in code block
+ code & {
+ font-family: var(--md-code-font-family);
+ font-size: inherit;
+ }
+
+ // Annotation is not hidden (e.g. when copying)
+ &:not([hidden]) {
+ display: inline-block;
+ // Hack: ensure that the line height doesn't exceed the line height of the
+ // hosting line, because it will lead to dancing pixels.
+ line-height: 1.25;
+ }
+
+ // Annotation index
+ &__index {
+ position: relative;
+ z-index: 0;
+ display: inline-block;
+ margin-inline: 0.4ch;
+ vertical-align: text-top;
+ cursor: pointer;
+ user-select: none;
+ outline: none;
+
+ // Hack: increase specificity to override default for anchors in typesetted
+ // content, because transitions are defined on anchor elements
+ .md-annotation & {
+ transition: z-index 250ms;
+ }
+
+ // Hack: Work around Firefox bug that renders a subpixel outline when
+ // rotating a mask image element.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1671784
+ overflow: hidden; // stylelint-disable-line order/properties-order
+ border-radius: 0.01px;
+
+ // [screen]: Render annotation markers as icons
+ @media screen {
+ width: 2.2ch;
+
+ // Annotation is visible
+ [data-md-visible] > & {
+ animation: pulse 2000ms infinite;
+ }
+
+ // Annotation marker background
+ &::before {
+ position: absolute;
+ top: -0.1ch;
+ z-index: -1;
+ width: 2.2ch;
+ height: 2.2ch;
+ content: "";
+ background: var(--md-default-bg-color);
+ mask-image: var(--md-annotation-bg-icon);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ // Annotation marker – the marker must be positioned absolutely behind
+ // the index, because it shouldn't impact the rendering of a code block.
+ // Otherwise, small rounding differences in browsers can sometimes mess up
+ // alignment of text following an annotation.
+ &::after {
+ position: absolute;
+ top: -0.1ch;
+ z-index: -1;
+ width: 2.2ch;
+ height: 2.2ch;
+ content: "";
+ background-color: var(--md-default-fg-color--lighter);
+ transition:
+ background-color 250ms,
+ transform 250ms;
+ // Hack: promote to own layer to reduce jitter
+ transform: scale(1.0001);
+ mask-image: var(--md-annotation-icon);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+
+ // Annotation marker for active tooltip
+ .md-tooltip--active + & {
+ transform: rotate(45deg);
+ }
+
+ // Annotation marker for active tooltip or on hover
+ :is(.md-tooltip--active + &, :hover > &) {
+ background-color: var(--md-accent-fg-color);
+ }
+ }
+ }
+
+ // Annotation index for active tooltip
+ .md-tooltip--active + & {
+ z-index: 2;
+ transition-duration: 0ms;
+ animation-play-state: paused;
+ }
+
+ // Annotation marker
+ [data-md-annotation-id] {
+ display: inline-block;
+
+ // [print]: Render annotation markers as numbers
+ @media print {
+ padding: 0 0.6ch;
+ font-weight: 700;
+ color: var(--md-default-bg-color);
+ white-space: nowrap;
+ background: var(--md-default-fg-color--lighter);
+ border-radius: 2ch;
+
+ // Annotation marker content
+ &::after {
+ content: attr(data-md-annotation-id);
+ }
+ }
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Annotation list
+ .md-annotation-list {
+ list-style: none;
+ counter-reset: xxx;
+
+ // Annotation list item
+ li {
+ position: relative;
+
+ // Annotation list marker
+ &::before {
+ position: absolute;
+ top: px2em(4px);
+ inset-inline-start: px2em(-34px);
+ min-width: 2ch;
+ height: 2ch;
+ padding: 0 0.6ch;
+ font-size: px2em(14.2px);
+ font-weight: 700;
+ line-height: 1.25;
+ color: var(--md-default-bg-color);
+ text-align: center;
+ content: counter(xxx);
+ counter-increment: xxx;
+ background: var(--md-default-fg-color--lighter);
+ border-radius: 2ch;
+ }
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_top.scss b/src/templates/assets/stylesheets/main/components/_top.scss
new file mode 100644
index 00000000..c24d44d1
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_top.scss
@@ -0,0 +1,83 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Back-to-top button
+.md-top {
+ position: fixed;
+ top: px2rem(48px + 16px);
+ z-index: 2;
+ display: block;
+ padding: px2rem(8px) px2rem(16px);
+ margin-inline-start: 50%;
+ font-size: px2rem(14px);
+ color: var(--md-default-fg-color--light);
+ cursor: pointer;
+ background-color: var(--md-default-bg-color);
+ border-radius: px2rem(32px);
+ outline: none;
+ box-shadow: var(--md-shadow-z2);
+ transition:
+ color 125ms,
+ background-color 125ms,
+ transform 125ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 125ms;
+ transform: translate(-50%, 0);
+
+ // [print]: Hide back-to-top button
+ @media print {
+ display: none;
+ }
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: translate(50%, 0);
+ }
+
+ // Back-to-top button is hidden
+ &[hidden] {
+ pointer-events: none;
+ opacity: 0;
+ transition-duration: 0ms;
+ transform: translate(-50%, px2rem(4px));
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: translate(50%, px2rem(4px));
+ }
+ }
+
+ // Back-to-top button on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-bg-color);
+ background-color: var(--md-accent-fg-color);
+ }
+
+ // Inline icon
+ svg {
+ display: inline-block;
+ vertical-align: -0.5em;
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/components/_version.scss b/src/templates/assets/stylesheets/main/components/_version.scss
new file mode 100644
index 00000000..3f85d6cd
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/components/_version.scss
@@ -0,0 +1,150 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Keyframes
+// ----------------------------------------------------------------------------
+
+// See https://github.com/squidfunk/mkdocs-material/issues/2429
+@keyframes hoverfix {
+ 0% {
+ pointer-events: none;
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Version selection variables
+:root {
+ --md-version-icon: svg-load("fontawesome/solid/caret-down.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Version selection
+.md-version {
+ flex-shrink: 0;
+ height: px2rem(48px);
+ font-size: px2rem(16px);
+
+ // Current selection
+ &__current {
+ position: relative;
+ // Hack: in general, we would use `vertical-align` to align the version at
+ // the bottom with the title, but since the list uses absolute positioning,
+ // this won't work consistently. Furthermore, we would need to use inline
+ // positioning to align the links, which looks jagged.
+ top: px2rem(1px);
+ margin-inline: px2rem(28px) px2rem(8px);
+ color: inherit;
+ cursor: pointer;
+ outline: none;
+
+ // Version selection icon
+ &::after {
+ display: inline-block;
+ width: px2rem(8px);
+ height: px2rem(12px);
+ margin-inline-start: px2rem(8px);
+ content: "";
+ background-color: currentcolor;
+ mask-image: var(--md-version-icon);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+ }
+
+ // Version selection list
+ &__list {
+ position: absolute;
+ top: px2rem(3px);
+ z-index: 3;
+ max-height: 0;
+ padding: 0;
+ margin: px2rem(4px) px2rem(16px);
+ overflow: auto;
+ color: var(--md-default-fg-color);
+ list-style-type: none;
+ background-color: var(--md-default-bg-color);
+ border-radius: px2rem(2px);
+ box-shadow: var(--md-shadow-z2);
+ opacity: 0;
+ transition:
+ max-height 0ms 500ms,
+ opacity 250ms 250ms;
+ scroll-snap-type: y mandatory;
+
+ // Version selection list on parent focus/hover
+ .md-version:is(:focus-within, :hover) & {
+ max-height: px2rem(200px);
+ opacity: 1;
+ transition:
+ max-height 0ms,
+ opacity 250ms;
+ }
+
+ // Fix hover on touch devices
+ @media (pointer: coarse), (hover: none) {
+ // Switch off on hover
+ .md-version:hover & {
+ animation: hoverfix 250ms forwards;
+ }
+
+ // Enable on focus
+ .md-version:focus-within & {
+ animation: none;
+ }
+ }
+ }
+
+ // Version selection item
+ &__item {
+ line-height: px2rem(36px);
+ }
+
+ // Version selection link
+ &__link {
+ display: block;
+ width: 100%;
+ padding-inline: px2rem(12px) px2rem(24px);
+ white-space: nowrap;
+ cursor: pointer;
+ outline: none;
+ transition:
+ color 250ms,
+ background-color 250ms;
+ scroll-snap-align: start;
+
+ // Link on focus/hover
+ &:is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+ }
+
+ // Link on focus
+ &:focus {
+ background-color: var(--md-default-fg-color--lightest);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss b/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss
new file mode 100644
index 00000000..bf517989
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss
@@ -0,0 +1,195 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+@use "sass:color";
+@use "sass:list";
+
+// ----------------------------------------------------------------------------
+// Variables
+// ----------------------------------------------------------------------------
+
+/// Admonition flavours
+$admonitions: (
+ "note": pencil-circle $clr-blue-a200,
+ "abstract": clipboard-text $clr-light-blue-a400,
+ "info": information $clr-cyan-a700,
+ "tip": fire $clr-teal-a700,
+ "success": check $clr-green-a700,
+ "question": help-circle $clr-light-green-a700,
+ "warning": alert $clr-orange-a400,
+ "failure": close $clr-red-a200,
+ "danger": lightning-bolt-circle $clr-red-a400,
+ "bug": shield-bug $clr-pink-a400,
+ "example": test-tube $clr-deep-purple-a200,
+ "quote": format-quote-close $clr-grey
+) !default;
+
+// ----------------------------------------------------------------------------
+// Rules: layout
+// ----------------------------------------------------------------------------
+
+// Admonition variables
+:root {
+ @each $name, $props in $admonitions {
+ --md-admonition-icon--#{$name}:
+ svg-load("material/#{list.nth($props, 1)}.svg");
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Admonition - note that all styles also apply to details tags, which are
+ // rendered as collapsible admonitions with summary elements as titles.
+ .admonition {
+ display: flow-root;
+ padding: 0 px2rem(12px);
+ margin: px2em(20px, 12.8px) 0;
+ font-size: px2rem(12.8px);
+ color: var(--md-admonition-fg-color);
+ background-color: var(--md-admonition-bg-color);
+ border: px2rem(1.5px) solid $clr-blue-a200;
+ border-radius: px2rem(4px);
+ box-shadow: var(--md-shadow-z1);
+ transition: box-shadow 125ms;
+ page-break-inside: avoid;
+
+ // [print]: Omit shadow as it may lead to rendering errors
+ @media print {
+ box-shadow: none;
+ }
+
+ // Admonition on focus
+ &:focus-within {
+ box-shadow: 0 0 0 px2rem(4px) color.adjust($clr-blue-a200, $alpha: -0.9);
+ }
+
+ // Hack: Chrome exhibits a weird issue where it will set nested elements to
+ // content-box. Doesn't happen in other browsers, so looks like a bug.
+ > * {
+ box-sizing: border-box;
+ }
+
+ // Adjust vertical spacing for nested admonitions
+ .admonition {
+ margin-top: 1em;
+ margin-bottom: 1em;
+ }
+
+ // Adjust spacing for contained table wrappers
+ .md-typeset__scrollwrap {
+ margin: 1em px2rem(-12px);
+ }
+
+ // Adjust spacing for contained tables
+ .md-typeset__table {
+ padding: 0 px2rem(12px);
+ }
+
+ // Adjust spacing for single-child tabbed block container
+ > .tabbed-set:only-child {
+ margin-top: 0;
+ }
+
+ // Adjust spacing on last child
+ html & > :last-child {
+ margin-bottom: px2rem(12px);
+ }
+ }
+
+ // Admonition title
+ .admonition-title {
+ position: relative;
+ padding-block: px2rem(8px);
+ padding-inline: px2rem(40px) px2rem(12px);
+ margin-block: 0;
+ margin-inline: px2rem(-12px);
+ font-weight: 700;
+ background-color: color.adjust($clr-blue-a200, $alpha: -0.9);
+ border: none;
+ border-inline-start-width: px2rem(4px);
+ border-start-start-radius: px2rem(2px);
+ border-start-end-radius: px2rem(2px);
+
+ // Adjust spacing for title-only admonitions
+ html &:last-child {
+ margin-bottom: 0;
+ }
+
+ // Admonition icon
+ &::before {
+ position: absolute;
+ top: px2em(10px);
+ width: px2rem(20px);
+ height: px2rem(20px);
+ content: "";
+ background-color: $clr-blue-a200;
+ inset-inline-start: px2rem(12px);
+ mask-image: var(--md-admonition-icon--note);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ // Inline code block
+ code {
+ box-shadow: 0 0 0 px2rem(1px) var(--md-default-fg-color--lightest);
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules: flavours
+// ----------------------------------------------------------------------------
+
+// Define admonition flavors
+@each $name, $props in $admonitions {
+ $tint: list.nth($props, 2);
+
+ // Admonition flavour
+ .md-typeset .admonition.#{$name} {
+ border-color: $tint;
+
+ // Admonition on focus
+ &:focus-within {
+ box-shadow: 0 0 0 px2rem(4px) color.adjust($tint, $alpha: -0.9);
+ }
+ }
+
+ // Admonition flavour title
+ .md-typeset .#{$name} > .admonition-title {
+ background-color: color.adjust($tint, $alpha: -0.9);
+
+ // Admonition icon
+ &::before {
+ background-color: $tint;
+ mask-image: var(--md-admonition-icon--#{$name});
+ }
+
+ // Details marker
+ &::after {
+ color: $tint;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss b/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss
new file mode 100644
index 00000000..59447d89
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss
@@ -0,0 +1,146 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Footnotes variables
+:root {
+ --md-footnotes-icon: svg-load("material/keyboard-return.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Footnote container
+ .footnote {
+ font-size: px2rem(12.8px);
+ color: var(--md-default-fg-color--light);
+
+ // Footnote list - omit left indentation
+ > ol {
+ margin-inline-start: 0;
+
+ // Footnote item - footnote items can contain lists, so we need to scope
+ // the spacing adjustments to the top-level footnote item.
+ > li {
+ transition: color 125ms;
+
+ // Darken color on target
+ &:target {
+ color: var(--md-default-fg-color);
+ }
+
+ // Show backreferences on footnote focus without transition
+ &:focus-within .footnote-backref {
+ opacity: 1;
+ transition: none;
+ transform: translateX(0);
+ }
+
+ // Show backreferences on footnote hover/target
+ &:is(:hover, :target) .footnote-backref {
+ opacity: 1;
+ transform: translateX(0);
+ }
+
+ // Adjust spacing on first child
+ > :first-child {
+ margin-top: 0;
+ }
+ }
+ }
+ }
+
+ // Footnote reference
+ .footnote-ref {
+ font-size: px2em(12px, 16px);
+ font-weight: 700;
+
+ // Hack: increase specificity to override default
+ html & {
+ outline-offset: px2rem(2px);
+ }
+ }
+
+ // Show outline for all devices
+ [id^="fnref:"]:target > .footnote-ref {
+ outline: auto;
+ }
+
+ // Footnote backreference
+ .footnote-backref {
+ display: inline-block;
+ // Hack: omit Unicode arrow for replacement with icon
+ font-size: 0;
+ color: var(--md-typeset-a-color);
+ vertical-align: text-bottom;
+ opacity: 0;
+ transition:
+ color 250ms,
+ transform 250ms 250ms,
+ opacity 125ms 250ms;
+ transform: translateX(px2rem(5px));
+
+ // [print]: Show footnote backreferences
+ @media print {
+ color: var(--md-typeset-a-color);
+ opacity: 1;
+ transform: translateX(0);
+ }
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: translateX(px2rem(-5px));
+ }
+
+ // Adjust color on hover
+ &:hover {
+ color: var(--md-accent-fg-color);
+ }
+
+ // Footnote backreference icon
+ &::before {
+ display: inline-block;
+ width: px2rem(16px);
+ height: px2rem(16px);
+ content: "";
+ background-color: currentcolor;
+ mask-image: var(--md-footnotes-icon);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+
+ // Flip icon vertically
+ svg {
+ transform: scaleX(-1);
+ }
+ }
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss b/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss
new file mode 100644
index 00000000..8284a5c0
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss
@@ -0,0 +1,92 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Headerlink
+ .headerlink {
+ display: inline-block;
+ margin-inline-start: px2rem(10px);
+ color: var(--md-default-fg-color--lighter);
+ opacity: 0;
+ transition:
+ color 250ms,
+ opacity 125ms;
+
+ // [print]: Hide headerlinks
+ @media print {
+ display: none;
+ }
+ }
+
+ // Show headerlinks on parent hover
+ :is(:hover, :target) > .headerlink,
+ .headerlink:focus {
+ opacity: 1;
+ transition:
+ color 250ms,
+ opacity 125ms;
+ }
+
+ // Adjust color on parent target or focus/hover
+ :target > .headerlink,
+ .headerlink:is(:focus, :hover) {
+ color: var(--md-accent-fg-color);
+ }
+
+ // Adjust scroll margin for all elements with `id` attributes
+ :target {
+ --md-scroll-margin: #{px2rem(48px + 24px)};
+ --md-scroll-offset: #{px2rem(0px)};
+ // Scroll margin is finally ready for prime time - before, we used a hack
+ // for anchor correction based on pseudo elements but those times are gone.
+ scroll-margin-top:
+ calc(
+ var(--md-scroll-margin) -
+ var(--md-scroll-offset)
+ );
+
+ // [screen +]: Sticky navigation tabs
+ @include break-from-device(screen) {
+
+ // Adjust scroll margin for sticky navigation tabs
+ .md-header--lifted ~ .md-container & {
+ --md-scroll-margin: #{px2rem(96px + 24px)};
+ }
+ }
+ }
+
+ // Adjust scroll offset for headlines of level 1-3
+ :is(h1, h2, h3):target {
+ --md-scroll-offset: #{px2rem(4px)};
+ }
+
+ // Adjust scroll offset for headlines of level 4
+ h4:target {
+ --md-scroll-offset: #{px2rem(3px)};
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss
new file mode 100644
index 00000000..fe8ffd62
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss
@@ -0,0 +1,52 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Arithmatex container
+ div.arithmatex {
+ overflow: auto;
+
+ // [mobile -]: Align with body copy
+ @include break-to-device(mobile) {
+ margin: 0 px2rem(-16px);
+ }
+
+ // Arithmatex content
+ > * {
+ width: min-content;
+ padding: 0 px2rem(16px);
+ margin-inline: auto !important; // stylelint-disable-line
+ touch-action: auto;
+
+ // MathJax container - see https://bit.ly/3HR8YJ5
+ mjx-container {
+ margin: 0 !important; // stylelint-disable-line
+ }
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss
new file mode 100644
index 00000000..683705ce
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss
@@ -0,0 +1,76 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Deletion
+ del.critic {
+ background-color: var(--md-typeset-del-color);
+ box-decoration-break: clone;
+ }
+
+ // Addition
+ ins.critic {
+ background-color: var(--md-typeset-ins-color);
+ box-decoration-break: clone;
+ }
+
+ // Comment
+ .critic.comment {
+ color: var(--md-code-hl-comment-color);
+ box-decoration-break: clone;
+
+ // Comment opening mark
+ &::before {
+ content: "/* ";
+ }
+
+ // Comment closing mark
+ &::after {
+ content: " */";
+ }
+ }
+
+ // Critic block
+ .critic.block {
+ display: block;
+ padding-inline: px2rem(16px);
+ margin: 1em 0;
+ overflow: auto;
+ box-shadow: none;
+
+ // Adjust spacing on first child
+ > :first-child {
+ margin-top: 0.5em;
+ }
+
+ // Adjust spacing on last child
+ > :last-child {
+ margin-bottom: 0.5em;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss
new file mode 100644
index 00000000..8eea678a
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss
@@ -0,0 +1,121 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Details variables
+:root {
+ --md-details-icon: svg-load("material/chevron-right.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Details
+ details {
+ @extend .admonition;
+
+ display: flow-root;
+ padding-top: 0;
+ overflow: visible;
+
+ // Details title icon - rotate icon on transition to open state
+ &[open] > summary::after {
+ transform: rotate(90deg);
+ }
+
+ // Adjust spacing for details in closed state
+ &:not([open]) {
+ padding-bottom: 0;
+ box-shadow: none;
+
+ // Hack: we cannot set `overflow: hidden` on the `details` element (which
+ // is why we set it to `overflow: visible`, as the outline would not be
+ // visible when focusing. Therefore, we must set the border radius on the
+ // summary explicitly.
+ > summary {
+ border-radius: px2rem(2px);
+ }
+ }
+ }
+
+ // Details title
+ summary {
+ @extend .admonition-title;
+
+ display: block;
+ min-height: px2rem(20px);
+ padding-inline-end: px2rem(36px);
+ cursor: pointer;
+ border-start-start-radius: px2rem(2px);
+ border-start-end-radius: px2rem(2px);
+
+ // Show outline for keyboard devices
+ &.focus-visible {
+ outline-color: var(--md-accent-fg-color);
+ outline-offset: px2rem(4px);
+ }
+
+ // Hide outline for pointer devices
+ &:not(.focus-visible) {
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ // Details marker
+ &::after {
+ position: absolute;
+ top: px2em(10px);
+ width: px2rem(20px);
+ height: px2rem(20px);
+ content: "";
+ background-color: currentcolor;
+ transition: transform 250ms;
+ transform: rotate(0deg);
+ inset-inline-end: px2rem(8px);
+ mask-image: var(--md-details-icon);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: rotate(180deg);
+ }
+ }
+
+ // Hide native details marker - modern
+ &::marker {
+ display: none;
+ }
+
+ // Hide native details marker - legacy, must be split into a seprate rule,
+ // so older browsers don't consider the selector list as invalid
+ &::-webkit-details-marker {
+ display: none;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss
new file mode 100644
index 00000000..8b351013
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss
@@ -0,0 +1,43 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Emoji and icon container
+ :is(.emojione, .twemoji, .gemoji) {
+ display: inline-flex;
+ height: px2em(18px);
+ vertical-align: text-top;
+
+ // Icon - inlined via mkdocs-material-extensions
+ svg {
+ width: px2em(18px);
+ max-height: 100%;
+ fill: currentcolor;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss
new file mode 100644
index 00000000..7d297677
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss
@@ -0,0 +1,382 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules: syntax highlighting
+// ----------------------------------------------------------------------------
+
+// Code block
+.highlight {
+
+ // .o = Operator
+ // .ow = Operator, word
+ :is(.o, .ow) {
+ color: var(--md-code-hl-operator-color);
+ }
+
+ .p { // Punctuation
+ color: var(--md-code-hl-punctuation-color);
+ }
+
+ // .cpf = Comment, preprocessor file
+ // .l = Literal
+ // .s = Literal, string
+ // .sb = Literal, string backticks
+ // .sc = Literal, string char
+ // .s2 = Literal, string double
+ // .si = Literal, string interpol
+ // .s1 = Literal, string single
+ // .ss = Literal, string symbol
+ :is(.cpf, .l, .s, .sb, .sc, .s2, .si, .s1, .ss) {
+ color: var(--md-code-hl-string-color);
+ }
+
+ // .cp = Comment, pre-processor
+ // .se = Literal, string escape
+ // .sh = Literal, string heredoc
+ // .sr = Literal, string regex
+ // .sx = Literal, string other
+ :is(.cp, .se, .sh, .sr, .sx) {
+ color: var(--md-code-hl-special-color);
+ }
+
+ // .m = Number
+ // .mb = Number, binary
+ // .mf = Number, float
+ // .mh = Number, hex
+ // .mi = Number, integer
+ // .il = Number, integer long
+ // .mo = Number, octal
+ :is(.m, .mb, .mf, .mh, .mi, .il, .mo) {
+ color: var(--md-code-hl-number-color);
+ }
+
+ // .k = Keyword,
+ // .kd = Keyword, declaration
+ // .kn = Keyword, namespace
+ // .kp = Keyword, pseudo
+ // .kr = Keyword, reserved
+ // .kt = Keyword, type
+ :is(.k, .kd, .kn, .kp, .kr, .kt) {
+ color: var(--md-code-hl-keyword-color);
+ }
+
+ // .kc = Keyword, constant
+ // .n = Name
+ :is(.kc, .n) {
+ color: var(--md-code-hl-name-color);
+ }
+
+ // .no = Name, constant
+ // .nb = Name, builtin
+ // .bp = Name, builtin pseudo
+ :is(.no, .nb, .bp) {
+ color: var(--md-code-hl-constant-color);
+ }
+
+ // .nc = Name, class
+ // .ne = Name, exception
+ // .nf = Name, function
+ // .nn = Name, namespace
+ :is(.nc, .ne, .nf, .nn) {
+ color: var(--md-code-hl-function-color);
+ }
+
+ // .nd = Name, decorator
+ // .ni = Name, entity
+ // .nl = Name, label
+ // .nt = Name, tag
+ :is(.nd, .ni, .nl, .nt) {
+ color: var(--md-code-hl-keyword-color);
+ }
+
+ // .c = Comment
+ // .cm = Comment, multiline
+ // .c1 = Comment, single
+ // .ch = Comment, shebang
+ // .cs = Comment, special
+ // .sd = Literal, string doc
+ :is(.c, .cm, .c1, .ch, .cs, .sd) {
+ color: var(--md-code-hl-comment-color);
+ }
+
+ // .na = Name, attribute
+ // .nv = Variable,
+ // .vc = Variable, class
+ // .vg = Variable, global
+ // .vi = Variable, instance
+ :is(.na, .nv, .vc, .vg, .vi) {
+ color: var(--md-code-hl-variable-color);
+ }
+
+ // .ge = Generic, emph
+ // .gr = Generic, error
+ // .gh = Generic, heading
+ // .go = Generic, output
+ // .gp = Generic, prompt
+ // .gs = Generic, strong
+ // .gu = Generic, subheading
+ // .gt = Generic, traceback
+ :is(.ge, .gr, .gh, .go, .gp, .gs, .gu, .gt) {
+ color: var(--md-code-hl-generic-color);
+ }
+
+ // .gd = Diff, delete
+ // .gi = Diff, insert
+ :is(.gd, .gi) {
+ padding: 0 px2em(2px);
+ margin: 0 px2em(-2px);
+ border-radius: px2rem(2px);
+ }
+
+ .gd { // Diff, delete
+ background-color: var(--md-typeset-del-color);
+ }
+
+ .gi { // Diff, insert
+ background-color: var(--md-typeset-ins-color);
+ }
+
+ // Highlighted line
+ .hll {
+ display: block;
+ padding: 0 px2em(16px, 13.6px);
+ margin: 0 px2em(-16px, 13.6px);
+ background-color: var(--md-code-hl-color--light);
+ box-shadow: 2px 0 0 0 var(--md-code-hl-color) inset;
+ }
+
+ // Code block title
+ span.filename {
+ position: relative;
+ display: flow-root;
+ padding: px2em(9px, 13.6px) px2em(16px, 13.6px);
+ margin-top: 1em;
+ font-size: px2em(13.6px);
+ font-weight: 700;
+ background-color: var(--md-code-bg-color);
+ border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest);
+ border-top-left-radius: px2rem(2px);
+ border-top-right-radius: px2rem(2px);
+
+ // Adjust spacing for code block
+ + pre {
+ margin-top: 0;
+
+ // Remove rounded border on top side
+ > code {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ }
+ }
+
+ // Code block line numbers (pymdownx-inline)
+ [data-linenos]::before {
+ position: sticky;
+ left: px2em(-16px, 13.6px);
+ // A `z-index` of 3 is necessary for ensuring that code block annotations
+ // don't overlay line numbers, as active annotations have a `z-index` of 2.
+ z-index: 3;
+ float: left;
+ padding-left: px2em(16px, 13.6px);
+ margin-right: px2em(16px, 13.6px);
+ margin-left: px2em(-16px, 13.6px);
+ color: var(--md-default-fg-color--light);
+ content: attr(data-linenos);
+ user-select: none;
+ background-color: var(--md-code-bg-color);
+ box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lightest) inset;
+ }
+
+ // Code block line anchors - Chrome and Safari seem to have a strange bug
+ // where scroll margin is not applied to anchors inside code blocks. Setting
+ // positioning to absolute seems to fix the problem. Interestingly, this does
+ // not happen in Firefox. Furthermore we must set `visibility: hidden` or
+ // the copy to clipboard functionality will include an empty line between
+ // each set of lines.
+ code a[id] {
+ position: absolute;
+ visibility: hidden;
+ }
+
+ // Copying in progress - this class is set before the content is copied and
+ // removed after copying is done to mitigate whitespace-related issues.
+ code[data-md-copying] {
+
+ // Temporarily remove highlighted lines - see https://bit.ly/32iVGWh
+ .hll {
+ display: contents;
+ }
+
+ // Temporarily remove annotations
+ .md-annotation {
+ display: none;
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules: layout
+// ----------------------------------------------------------------------------
+
+// Code block with line numbers
+.highlighttable {
+ display: flow-root;
+
+ // Set table elements to block layout, because otherwise the whole flexbox
+ // hacking won't work correctly
+ :is(tbody, td) {
+ display: block;
+ padding: 0;
+ }
+
+ // We need to use flexbox layout, because otherwise it's not possible to
+ // make the code container scroll while keeping the line numbers static
+ tr {
+ display: flex;
+ }
+
+ // The pre tags are nested inside a table, so we need to omit the margin
+ // because it collapses below all the overflows
+ pre {
+ margin: 0;
+ }
+
+ // Code block title container
+ th.filename {
+ flex-grow: 1;
+ padding: 0;
+ text-align: left;
+
+ // Adjust spacing
+ span.filename {
+ margin-top: 0;
+ }
+ }
+
+ // Code block line numbers - disable user selection, so code can be easily
+ // copied without accidentally also copying the line numbers
+ .linenos {
+ padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px);
+ padding-right: 0;
+ font-size: px2em(13.6px);
+ user-select: none;
+ background-color: var(--md-code-bg-color);
+ border-top-left-radius: px2rem(2px);
+ border-bottom-left-radius: px2rem(2px);
+ }
+
+ // Code block line numbers container
+ .linenodiv {
+ padding-right: px2em(8px, 13.6px);
+ box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lightest) inset;
+
+ // Adjust colors and alignment
+ pre {
+ color: var(--md-default-fg-color--light);
+ text-align: right;
+ }
+ }
+
+ // Code block container - stretch to remaining space
+ .code {
+ flex: 1;
+ min-width: 0;
+ }
+}
+
+// Code block line numbers container
+.linenodiv a {
+ color: inherit;
+}
+
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Code block with line numbers - unfortunately, these selectors need to be
+ // overly specific so they don't bleed into code blocks in annotations.
+ .highlighttable {
+ margin: 1em 0;
+ direction: ltr;
+
+ // Remove rounded borders on code blocks
+ > tbody > tr > .code > div > pre > code {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+
+ // Code block result container
+ .highlight + .result {
+ padding: 0 px2em(16px);
+ margin-top: calc(-1em + #{px2em(-2px)});
+ overflow: visible;
+ border: px2rem(1px) solid var(--md-code-bg-color);
+ border-top-width: px2rem(2px);
+ border-bottom-right-radius: px2rem(2px);
+ border-bottom-left-radius: px2rem(2px);
+
+ // Clearfix, because we can't use overflow: auto
+ &::after {
+ display: block;
+ clear: both;
+ content: "";
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules: top-level
+// ----------------------------------------------------------------------------
+
+// [mobile -]: Align with body copy
+@include break-to-device(mobile) {
+
+ // Top-level code block
+ .md-content__inner > .highlight {
+ margin: 1em px2rem(-16px);
+
+ // Remove rounded borders
+ > .filename,
+ > pre > code {
+ border-radius: 0;
+ }
+
+ // Code block with line numbers - unfortunately, these selectors need to be
+ // overly specific so they don't bleed into code blocks in annotations.
+ > .highlighttable > tbody > tr > .filename span.filename,
+ > .highlighttable > tbody > tr > .linenos,
+ > .highlighttable > tbody > tr > .code > div > pre > code {
+ border-radius: 0;
+ }
+
+ // Code block result container
+ + .result {
+ margin-inline: px2rem(-16px);
+ border-inline-width: 0;
+ border-radius: 0;
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss
new file mode 100644
index 00000000..8749f08c
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss
@@ -0,0 +1,115 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Keyboard key
+ .keys {
+
+ // Keyboard key icon
+ kbd:is(::before, ::after) {
+ position: relative;
+ margin: 0;
+ color: inherit;
+ -moz-osx-font-smoothing: initial;
+ -webkit-font-smoothing: initial;
+ }
+
+ // Surrounding text
+ span {
+ padding: 0 px2em(3.2px);
+ color: var(--md-default-fg-color--light);
+ }
+
+ // Define keyboard keys with left icon
+ @each $name, $code in (
+
+ // Modifiers
+ "alt": "\2387",
+ "left-alt": "\2387",
+ "right-alt": "\2387",
+ "command": "\2318",
+ "left-command": "\2318",
+ "right-command": "\2318",
+ "control": "\2303",
+ "left-control": "\2303",
+ "right-control": "\2303",
+ "meta": "\25C6",
+ "left-meta": "\25C6",
+ "right-meta": "\25C6",
+ "option": "\2325",
+ "left-option": "\2325",
+ "right-option": "\2325",
+ "shift": "\21E7",
+ "left-shift": "\21E7",
+ "right-shift": "\21E7",
+ "super": "\2756",
+ "left-super": "\2756",
+ "right-super": "\2756",
+ "windows": "\229E",
+ "left-windows": "\229E",
+ "right-windows": "\229E",
+
+ // Other keys
+ "arrow-down": "\2193",
+ "arrow-left": "\2190",
+ "arrow-right": "\2192",
+ "arrow-up": "\2191",
+ "backspace": "\232B",
+ "backtab": "\21E4",
+ "caps-lock": "\21EA",
+ "clear": "\2327",
+ "context-menu": "\2630",
+ "delete": "\2326",
+ "eject": "\23CF",
+ "end": "\2913",
+ "escape": "\238B",
+ "home": "\2912",
+ "insert": "\2380",
+ "page-down": "\21DF",
+ "page-up": "\21DE",
+ "print-screen": "\2399"
+ ) {
+ .key-#{$name}::before {
+ padding-right: px2em(6.4px);
+ content: $code;
+ }
+ }
+
+ // Define keyboard keys with right icon
+ @each $name, $code in (
+ "tab": "\21E5",
+ "num-enter": "\2324",
+ "enter": "\23CE"
+ ) {
+ .key-#{$name}::after {
+ padding-left: px2em(6.4px);
+ content: $code;
+ }
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss
new file mode 100644
index 00000000..9df91bfc
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss
@@ -0,0 +1,400 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Tabbed variables
+:root {
+ --md-tabbed-icon--prev: svg-load("material/chevron-left.svg");
+ --md-tabbed-icon--next: svg-load("material/chevron-right.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Tabbed container
+ .tabbed-set {
+ position: relative;
+ display: flex;
+ flex-flow: column wrap;
+ margin: 1em 0;
+ border-radius: px2rem(2px);
+
+ // Tab radio button - the Tabbed extension will generate radio buttons with
+ // labels, so tabs can be triggered without the necessity for JavaScript.
+ // This is pretty cool, as it has great accessibility out-of-the box, so
+ // we just hide the radio button and toggle the label color for indication.
+ > input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+
+ // Adjust scroll margin
+ &:target {
+ --md-scroll-offset: #{px2em(10px, 16px)};
+ }
+
+ // Tab label states
+ @for $i from 20 through 1 {
+ &:nth-child(#{$i}) {
+
+ // Tab is active
+ &:checked {
+
+ // Tab label
+ ~ .tabbed-labels > :nth-child(#{$i}) {
+ @extend %tabbed-label;
+ }
+
+ // Tab content
+ ~ .tabbed-content > :nth-child(#{$i}) {
+ @extend %tabbed-content;
+ }
+ }
+
+ // Tab label on keyboard focus
+ &.focus-visible ~ .tabbed-labels > :nth-child(#{$i}) {
+ @extend %tabbed-label-focus-visible;
+ }
+ }
+ }
+
+ // Tab indicator on keyboard focus
+ &.focus-visible ~ .tabbed-labels::before {
+ background-color: var(--md-accent-fg-color);
+ }
+ }
+ }
+
+ // Tabbed labels
+ .tabbed-labels {
+ display: flex;
+ max-width: 100%;
+ overflow: auto;
+ box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest) inset;
+ -ms-overflow-style: none; // IE, Edge
+ scrollbar-width: none; // Firefox
+
+ // [print]: Move one layer up for ordering
+ @media print {
+ display: contents;
+ }
+
+ // [screen and no reduced motion]: Disable animation
+ @media screen {
+
+ // [js]: Show animated tab indicator
+ .js & {
+ position: relative;
+
+ // Tab indicator
+ &::before {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ display: block;
+ width: var(--md-indicator-width);
+ height: 2px;
+ content: "";
+ background: var(--md-default-fg-color);
+ transition:
+ width 225ms,
+ background-color 250ms,
+ transform 250ms;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transform: translateX(var(--md-indicator-x));
+ }
+ }
+ }
+
+ // Webkit scrollbar
+ &::-webkit-scrollbar {
+ display: none; // Chrome, Safari
+ }
+
+ // Tab label
+ > label {
+ flex-shrink: 0;
+ width: auto;
+ padding: px2em(10px, 12.8px) 1.25em px2em(8px, 12.8px);
+ font-size: px2rem(12.8px);
+ font-weight: 700;
+ color: var(--md-default-fg-color--light);
+ white-space: nowrap;
+ cursor: pointer;
+ border-bottom: px2rem(2px) solid transparent;
+ border-radius: px2rem(2px) px2rem(2px) 0 0;
+ transition:
+ background-color 250ms,
+ color 250ms;
+ scroll-margin-inline-start: px2rem(20px);
+
+ // [print]: Intersperse labels with containers
+ @media print {
+
+ // Ensure correct order of labels
+ @for $i from 1 through 20 {
+ &:nth-child(#{$i}) {
+ order: $i;
+ }
+ }
+ }
+
+ // Tab label on hover
+ &:hover {
+ color: var(--md-default-fg-color);
+ }
+ }
+ }
+
+ // Tabbed content
+ .tabbed-content {
+ width: 100%;
+
+ // [print]: Move one layer up for ordering
+ @media print {
+ display: contents;
+ }
+ }
+
+ // Tabbed block
+ .tabbed-block {
+ display: none;
+
+ // [print]: Intersperse labels with containers
+ @media print {
+ display: block;
+
+ // Ensure correct order of containers
+ @for $i from 1 through 20 {
+ &:nth-child(#{$i}) {
+ order: $i;
+ }
+ }
+ }
+
+ // Code block is the first child of a tab - remove margin and mirror
+ // previous (now deprecated) SuperFences code block grouping behavior
+ > pre:first-child,
+ > .highlight:first-child > pre {
+ margin: 0;
+
+ // Remove rounded borders on code block
+ > code {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ }
+
+ // Code block is the first child of a tab - remove margin and mirror
+ // previous (now deprecated) SuperFences code block grouping behavior
+ > .highlight:first-child {
+
+ // Code block title - remove spacing and rounded borders
+ > .filename {
+ margin: 0;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ // Code block with line numbers - unfortunately, these selectors need to
+ // be overly specific so they don't bleed into code blocks in annotations.
+ > .highlighttable {
+ margin: 0;
+
+ // Remove rounded borders on line numbers and titles
+ > tbody > tr > .filename span.filename,
+ > tbody > tr > .linenos {
+ margin: 0;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ // Remove rounded borders on code blocks
+ > tbody > tr > .code > div > pre > code {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ }
+
+ // Code block result container - adjust spacing
+ + .result {
+ margin-top: px2em(-2px);
+ }
+ }
+
+ // Adjust spacing for nested tabbed container
+ > .tabbed-set {
+ margin: 0;
+ }
+ }
+
+ // Tabbed button
+ .tabbed-button {
+ display: block;
+ align-self: center;
+ width: px2rem(18px);
+ height: px2rem(18px);
+ margin-top: px2rem(2px);
+ color: var(--md-default-fg-color--light);
+ pointer-events: initial;
+ cursor: pointer;
+ border-radius: 100%;
+ transition: background-color 250ms;
+
+ // Tabbed button on hover
+ &:hover {
+ color: var(--md-accent-fg-color);
+ background-color: var(--md-accent-fg-color--transparent);
+ }
+
+ // Tabbed button icon
+ &::after {
+ display: block;
+ width: 100%;
+ height: 100%;
+ content: "";
+ background-color: currentcolor;
+ transition:
+ background-color 250ms,
+ transform 250ms;
+ mask-image: var(--md-tabbed-icon--prev);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+ }
+
+ // Tabbed control
+ .tabbed-control {
+ position: absolute;
+ display: flex;
+ justify-content: start;
+ width: px2rem(24px);
+ height: px2rem(38px);
+ pointer-events: none;
+ background:
+ linear-gradient(
+ to right,
+ var(--md-default-bg-color) 60%,
+ transparent
+ );
+ transition: opacity 125ms;
+
+ // Adjust for right-to-left languages
+ [dir="rtl"] & {
+ transform: rotate(180deg);
+ }
+
+ // Tabbed control is hidden
+ &[hidden] {
+ opacity: 0;
+ }
+
+ // Tabbed control next
+ &--next {
+ right: 0;
+ justify-content: end;
+ background:
+ linear-gradient(
+ to left,
+ var(--md-default-bg-color) 60%,
+ transparent
+ );
+
+ // Tabbed button icon content
+ .tabbed-button::after {
+ mask-image: var(--md-tabbed-icon--next);
+ }
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules: top-level
+// ----------------------------------------------------------------------------
+
+// [mobile -]: Align with body copy
+@include break-to-device(mobile) {
+
+ // Top-level tabbed labels
+ .md-content__inner > .tabbed-set .tabbed-labels {
+ max-width: 100vw;
+ padding-inline-start: px2rem(16px);
+ margin: 0 px2rem(-16px);
+ scroll-padding-inline-start: px2rem(16px);
+
+ // Hack: some browsers ignore the right padding on flex containers,
+ // see https://bit.ly/3lsPS3S
+ &::after {
+ padding-inline-end: px2rem(16px);
+ content: "";
+ }
+
+ // Tabbed control previous
+ ~ .tabbed-control--prev {
+ width: px2rem(40px);
+ padding-inline-start: px2rem(16px);
+ margin-inline-start: px2rem(-16px);
+ }
+
+ // Tabbed control next
+ ~ .tabbed-control--next {
+ width: px2rem(40px);
+ padding-inline-end: px2rem(16px);
+ margin-inline-end: px2rem(-16px);
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Placeholders: improve colocation for better compression
+// ----------------------------------------------------------------------------
+
+// Tab label placeholder
+%tabbed-label {
+
+ // [screen]: Show active state
+ @media screen {
+ color: var(--md-default-fg-color);
+
+ // [no-js]: Show border (indicator is animated with JavaScript)
+ .no-js & {
+ border-color: var(--md-default-fg-color);
+ }
+ }
+}
+
+// Tab label on keyboard focus placeholder
+%tabbed-label-focus-visible {
+ color: var(--md-accent-fg-color);
+}
+
+// Tab content placeholder
+%tabbed-content {
+ display: block;
+}
diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss
new file mode 100644
index 00000000..a1d1117c
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss
@@ -0,0 +1,78 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Tasklist variables
+:root {
+ --md-tasklist-icon: svg-load("octicons/check-circle-fill-24.svg");
+ --md-tasklist-icon--checked: svg-load("octicons/check-circle-fill-24.svg");
+}
+
+// ----------------------------------------------------------------------------
+
+// Scoped in typesetted content to match specificity of regular content
+.md-typeset {
+
+ // Tasklist item
+ .task-list-item {
+ position: relative;
+ list-style-type: none;
+
+ // Make checkbox items align with normal list items, but position
+ // everything in ems for correct layout at smaller font sizes
+ [type="checkbox"] {
+ position: absolute;
+ top: 0.45em;
+ inset-inline-start: -2em;
+ }
+ }
+
+ // Hide native checkbox, when custom classes are enabled
+ .task-list-control [type="checkbox"] {
+ z-index: -1;
+ opacity: 0;
+ }
+
+ // Tasklist indicator in unchecked state
+ .task-list-indicator::before {
+ position: absolute;
+ top: 0.15em;
+ width: px2em(20px);
+ height: px2em(20px);
+ content: "";
+ background-color: var(--md-default-fg-color--lightest);
+ inset-inline-start: px2em(-24px);
+ mask-image: var(--md-tasklist-icon);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ // Tasklist indicator in checked state
+ [type="checkbox"]:checked + .task-list-indicator::before {
+ background-color: $clr-green-a400;
+ mask-image: var(--md-tasklist-icon--checked);
+ }
+}
diff --git a/src/templates/assets/stylesheets/main/integrations/_mermaid.scss b/src/templates/assets/stylesheets/main/integrations/_mermaid.scss
new file mode 100644
index 00000000..d0325f39
--- /dev/null
+++ b/src/templates/assets/stylesheets/main/integrations/_mermaid.scss
@@ -0,0 +1,67 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Mermaid variables
+:root > * {
+ --md-mermaid-font-family: var(--md-text-font-family), sans-serif;
+
+ // General colors
+ --md-mermaid-edge-color: var(--md-code-fg-color);
+ --md-mermaid-node-bg-color: var(--md-accent-fg-color--transparent);
+ --md-mermaid-node-fg-color: var(--md-accent-fg-color);
+ --md-mermaid-label-bg-color: var(--md-default-bg-color);
+ --md-mermaid-label-fg-color: var(--md-code-fg-color);
+
+ // Sequence diagram colors
+ --md-mermaid-sequence-actor-bg-color: var(--md-mermaid-label-bg-color);
+ --md-mermaid-sequence-actor-fg-color: var(--md-mermaid-label-fg-color);
+ --md-mermaid-sequence-actor-border-color: var(--md-mermaid-node-fg-color);
+ --md-mermaid-sequence-actor-line-color: var(--md-default-fg-color--lighter);
+ --md-mermaid-sequence-actorman-bg-color: var(--md-mermaid-label-bg-color);
+ --md-mermaid-sequence-actorman-line-color: var(--md-mermaid-node-fg-color);
+ --md-mermaid-sequence-box-bg-color: var(--md-mermaid-node-bg-color);
+ --md-mermaid-sequence-box-fg-color: var(--md-mermaid-edge-color);
+ --md-mermaid-sequence-label-bg-color: var(--md-mermaid-node-bg-color);
+ --md-mermaid-sequence-label-fg-color: var(--md-mermaid-node-fg-color);
+ --md-mermaid-sequence-loop-bg-color: var(--md-mermaid-node-bg-color);
+ --md-mermaid-sequence-loop-fg-color: var(--md-mermaid-edge-color);
+ --md-mermaid-sequence-loop-border-color: var(--md-mermaid-node-fg-color);
+ --md-mermaid-sequence-message-fg-color: var(--md-mermaid-edge-color);
+ --md-mermaid-sequence-message-line-color: var(--md-mermaid-edge-color);
+ --md-mermaid-sequence-note-bg-color: var(--md-mermaid-label-bg-color);
+ --md-mermaid-sequence-note-fg-color: var(--md-mermaid-edge-color);
+ --md-mermaid-sequence-note-border-color: var(--md-mermaid-label-fg-color);
+ --md-mermaid-sequence-number-bg-color: var(--md-mermaid-node-fg-color);
+ --md-mermaid-sequence-number-fg-color: var(--md-accent-bg-color);
+}
+
+// ----------------------------------------------------------------------------
+
+// Mermaid container
+.mermaid {
+ margin: 1em 0;
+ line-height: normal;
+}
diff --git a/src/templates/assets/stylesheets/palette.scss b/src/templates/assets/stylesheets/palette.scss
new file mode 100644
index 00000000..ff73a982
--- /dev/null
+++ b/src/templates/assets/stylesheets/palette.scss
@@ -0,0 +1,40 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Dependencies
+// ----------------------------------------------------------------------------
+
+@import "material-color";
+
+// ----------------------------------------------------------------------------
+// Local imports
+// ----------------------------------------------------------------------------
+
+@import "utilities/break";
+@import "utilities/convert";
+
+@import "config";
+
+@import "palette/scheme";
+@import "palette/accent";
+@import "palette/primary";
diff --git a/src/templates/assets/stylesheets/palette/_accent.scss b/src/templates/assets/stylesheets/palette/_accent.scss
new file mode 100644
index 00000000..9f69b596
--- /dev/null
+++ b/src/templates/assets/stylesheets/palette/_accent.scss
@@ -0,0 +1,61 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Define accent colors
+@each $name, $color in (
+ "red": $clr-red-a400,
+ "pink": $clr-pink-a400,
+ "purple": $clr-purple-a200,
+ "deep-purple": $clr-deep-purple-a200,
+ "indigo": $clr-indigo-a200,
+ "blue": $clr-blue-a200,
+ "light-blue": $clr-light-blue-a700,
+ "cyan": $clr-cyan-a700,
+ "teal": $clr-teal-a700,
+ "green": $clr-green-a700,
+ "light-green": $clr-light-green-a700,
+ "lime": $clr-lime-a700,
+ "yellow": $clr-yellow-a700,
+ "amber": $clr-amber-a700,
+ "orange": $clr-orange-a400,
+ "deep-orange": $clr-deep-orange-a200
+) {
+
+ // Color palette
+ [data-md-color-accent="#{$name}"] {
+ --md-accent-fg-color: hsla(#{hex2hsl($color)}, 1);
+ --md-accent-fg-color--transparent: hsla(#{hex2hsl($color)}, 0.1);
+
+ // Inverted text for lighter shades
+ @if index("lime" "yellow" "amber" "orange", $name) {
+ --md-accent-bg-color: hsla(0, 0%, 0%, 0.87);
+ --md-accent-bg-color--light: hsla(0, 0%, 0%, 0.54);
+ } @else {
+ --md-accent-bg-color: hsla(0, 0%, 100%, 1);
+ --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/palette/_primary.scss b/src/templates/assets/stylesheets/palette/_primary.scss
new file mode 100644
index 00000000..a8653f0f
--- /dev/null
+++ b/src/templates/assets/stylesheets/palette/_primary.scss
@@ -0,0 +1,203 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+@use "sass:list";
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Define primary colors
+@each $name, $colors in (
+ "red": $clr-red-400 $clr-red-300 $clr-red-600,
+ "pink": $clr-pink-500 $clr-pink-400 $clr-pink-700,
+ "purple": $clr-purple-400 $clr-purple-300 $clr-purple-600,
+ "deep-purple": $clr-deep-purple-400 $clr-deep-purple-300 $clr-deep-purple-500,
+ "indigo": $clr-indigo-500 $clr-indigo-400 $clr-indigo-700,
+ "blue": $clr-blue-500 $clr-blue-400 $clr-blue-700,
+ "light-blue": $clr-light-blue-500 $clr-light-blue-400 $clr-light-blue-700,
+ "cyan": $clr-cyan-500 $clr-cyan-400 $clr-cyan-700,
+ "teal": $clr-teal-500 $clr-teal-400 $clr-teal-700,
+ "green": $clr-green-500 $clr-green-400 $clr-green-700,
+ "light-green": $clr-light-green-500 $clr-light-green-400 $clr-light-green-700,
+ "lime": $clr-lime-500 $clr-lime-400 $clr-lime-700,
+ "yellow": $clr-yellow-500 $clr-yellow-400 $clr-yellow-700,
+ "amber": $clr-amber-500 $clr-amber-400 $clr-amber-700,
+ "orange": $clr-orange-400 $clr-orange-400 $clr-orange-600,
+ "deep-orange": $clr-deep-orange-400 $clr-deep-orange-300 $clr-deep-orange-600,
+ "brown": $clr-brown-500 $clr-brown-400 $clr-brown-700,
+ "grey": $clr-grey-600 $clr-grey-500 $clr-grey-700,
+ "blue-grey": $clr-blue-grey-600 $clr-blue-grey-500 $clr-blue-grey-700
+) {
+
+ // Color palette
+ [data-md-color-primary="#{$name}"] {
+ --md-primary-fg-color: hsl(#{hex2hsl(list.nth($colors, 1))});
+ --md-primary-fg-color--light: hsl(#{hex2hsl(list.nth($colors, 2))});
+ --md-primary-fg-color--dark: hsl(#{hex2hsl(list.nth($colors, 3))});
+
+ // Inverted text for lighter shades
+ @if index("lime" "yellow" "amber" "orange", $name) {
+ --md-primary-bg-color: hsla(0, 0%, 0%, 0.87);
+ --md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54);
+ } @else {
+ --md-primary-bg-color: hsla(0, 0%, 100%, 1);
+ --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);
+ }
+
+ // Typeset color shades
+ @if index("grey" "blue-grey", $name) {
+ --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)});
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+// Adjust link colors for light primary colors
+@each $name, $color in (
+ "light-green": hsl(88, 58%, 43%),
+ "lime": hsl(66, 88%, 32%),
+ "yellow": hsl(54, 100%, 36%),
+ "amber": hsl(45, 100%, 41%),
+ "orange": hsl(36, 100%, 45%)
+) {
+ [data-md-color-primary="#{$name}"]:not([data-md-color-scheme="slate"]) {
+ --md-typeset-a-color: #{$color};
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules: white
+// ----------------------------------------------------------------------------
+
+// Define primary colors for white
+[data-md-color-primary="white"] {
+ --md-primary-fg-color: hsla(var(--md-hue), 0%, 100%, 1);
+ --md-primary-fg-color--light: hsla(var(--md-hue), 0%, 100%, 0.7);
+ --md-primary-fg-color--dark: hsla(var(--md-hue), 0%, 0%, 0.07);
+ --md-primary-bg-color: hsla(var(--md-hue), 0%, 0%, 0.87);
+ --md-primary-bg-color--light: hsla(var(--md-hue), 0%, 0%, 0.54);
+
+ // Typeset `a` color shades
+ --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)});
+
+ // Form button
+ .md-button {
+ color: var(--md-typeset-a-color);
+
+ // Primary button
+ &--primary {
+ color: hsla(var(--md-hue), 0%, 100%, 1);
+ background-color: var(--md-typeset-a-color);
+ border-color: var(--md-typeset-a-color);
+ }
+ }
+
+ // [tablet portrait +]: Header-embedded search
+ @include break-from-device(tablet landscape) {
+
+ // Search form
+ .md-search__form {
+ background-color: hsla(var(--md-hue), 0%, 0%, 0.07);
+
+ // Search form on hover
+ &:hover {
+ background-color: hsla(var(--md-hue), 0%, 0%, 0.32);
+ }
+ }
+
+ // Search icon
+ .md-search__input + .md-search__icon {
+ color: hsla(var(--md-hue), 0%, 0%, 0.87);
+ }
+ }
+
+ // [screen +]: Add bottom border for tabs
+ @include break-from-device(screen) {
+
+ // Navigation tabs
+ .md-tabs {
+ border-bottom: px2rem(1px) solid hsla(0, 0%, 0%, 0.07);
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Rules: black
+// ----------------------------------------------------------------------------
+
+// Define primary colors for black
+[data-md-color-primary="black"] {
+ --md-primary-fg-color: hsla(var(--md-hue), 15%, 9%, 1);
+ --md-primary-fg-color--light: hsla(var(--md-hue), 15%, 9%, 0.54);
+ --md-primary-fg-color--dark: hsla(var(--md-hue), 15%, 9%, 1);
+ --md-primary-bg-color: hsla(var(--md-hue), 15%, 100%, 1);
+ --md-primary-bg-color--light: hsla(var(--md-hue), 15%, 100%, 0.7);
+
+ // Typeset `a` color shades
+ --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)});
+
+ // Form button
+ .md-button {
+ color: var(--md-typeset-a-color);
+
+ // Primary button
+ &--primary {
+ color: hsla(var(--md-hue), 0%, 100%, 1);
+ background-color: var(--md-typeset-a-color);
+ border-color: var(--md-typeset-a-color);
+ }
+ }
+
+ // Header
+ .md-header {
+ background-color: hsla(var(--md-hue), 15%, 9%, 1);
+ }
+
+ // [tablet portrait -]: Layered navigation
+ @include break-to-device(tablet portrait) {
+
+ // Repository information container
+ .md-nav__source {
+ background-color: hsla(var(--md-hue), 15%, 11%, 0.87);
+ }
+ }
+
+ // [tablet -]: Layered navigation
+ @include break-to-device(tablet) {
+
+ // Site title in main navigation
+ html & .md-nav--primary .md-nav__title[for="__drawer"] {
+ background-color: hsla(var(--md-hue), 15%, 9%, 1);
+ }
+ }
+
+ // [screen +]: Set background color for tabs
+ @include break-from-device(screen) {
+
+ // Navigation tabs
+ .md-tabs {
+ background-color: hsla(var(--md-hue), 15%, 9%, 1);
+ }
+ }
+}
diff --git a/src/templates/assets/stylesheets/palette/_scheme.scss b/src/templates/assets/stylesheets/palette/_scheme.scss
new file mode 100644
index 00000000..0a9f9823
--- /dev/null
+++ b/src/templates/assets/stylesheets/palette/_scheme.scss
@@ -0,0 +1,145 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+// ----------------------------------------------------------------------------
+// Rules
+// ----------------------------------------------------------------------------
+
+// Only use dark mode on screens
+@media screen {
+
+ // Slate theme, i.e. dark mode
+ [data-md-color-scheme="slate"] {
+
+ // Indicate that the site is rendered with a dark color scheme
+ color-scheme: dark;
+
+ // Default color shades
+ --md-default-fg-color: hsla(var(--md-hue), 15%, 90%, 0.82);
+ --md-default-fg-color--light: hsla(var(--md-hue), 15%, 90%, 0.56);
+ --md-default-fg-color--lighter: hsla(var(--md-hue), 15%, 90%, 0.32);
+ --md-default-fg-color--lightest: hsla(var(--md-hue), 15%, 90%, 0.12);
+ --md-default-bg-color: hsla(var(--md-hue), 15%, 14%, 1);
+ --md-default-bg-color--light: hsla(var(--md-hue), 15%, 14%, 0.54);
+ --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 14%, 0.26);
+ --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 14%, 0.07);
+
+ // Code color shades
+ --md-code-fg-color: hsla(var(--md-hue), 18%, 86%, 0.82);
+ --md-code-bg-color: hsla(var(--md-hue), 15%, 18%, 1);
+
+ // Code highlighting color shades
+ --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a200)}, 0.15);
+ --md-code-hl-number-color: hsla(6, 74%, 63%, 1);
+ --md-code-hl-special-color: hsla(340, 83%, 66%, 1);
+ --md-code-hl-function-color: hsla(291, 57%, 65%, 1);
+ --md-code-hl-constant-color: hsla(250, 62%, 70%, 1);
+ --md-code-hl-keyword-color: hsla(219, 66%, 64%, 1);
+ --md-code-hl-string-color: hsla(150, 58%, 44%, 1);
+ --md-code-hl-name-color: var(--md-code-fg-color);
+ --md-code-hl-operator-color: var(--md-default-fg-color--light);
+ --md-code-hl-punctuation-color: var(--md-default-fg-color--light);
+ --md-code-hl-comment-color: var(--md-default-fg-color--light);
+ --md-code-hl-generic-color: var(--md-default-fg-color--light);
+ --md-code-hl-variable-color: var(--md-default-fg-color--light);
+
+ // Typeset color shades
+ --md-typeset-color: var(--md-default-fg-color);
+
+ // Typeset `a` color shades
+ --md-typeset-a-color: var(--md-primary-fg-color);
+
+ // Typeset `kbd` color shades
+ --md-typeset-kbd-color: hsla(var(--md-hue), 15%, 90%, 0.12);
+ --md-typeset-kbd-accent-color: hsla(var(--md-hue), 15%, 90%, 0.2);
+ --md-typeset-kbd-border-color: hsla(var(--md-hue), 15%, 14%, 1);
+
+ // Typeset `mark` color shades
+ --md-typeset-mark-color: hsla(#{hex2hsl($clr-blue-a200)}, 0.3);
+
+ // Typeset `table` color shades
+ --md-typeset-table-color: hsla(var(--md-hue), 15%, 95%, 0.12);
+ --md-typeset-table-color--light: hsla(var(--md-hue), 15%, 95%, 0.035);
+
+ // Admonition color shades
+ --md-admonition-fg-color: var(--md-default-fg-color);
+ --md-admonition-bg-color: var(--md-default-bg-color);
+
+ // Footer color shades
+ --md-footer-bg-color: hsla(var(--md-hue), 15%, 10%, 0.87);
+ --md-footer-bg-color--dark: hsla(var(--md-hue), 15%, 8%, 1);
+
+ // Shadow depth 1
+ --md-shadow-z1:
+ 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05),
+ 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1);
+
+ // Shadow depth 2
+ --md-shadow-z2:
+ 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.25),
+ 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25);
+
+ // Shadow depth 3
+ --md-shadow-z3:
+ 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.4),
+ 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35);
+
+ // Hide images for light mode
+ img[src$="#only-light"],
+ img[src$="#gh-light-mode-only"] {
+ display: none;
+ }
+ }
+
+ // --------------------------------------------------------------------------
+
+ // Adjust link colors for dark primary colors
+ @each $name, $color in (
+ "pink": hsl(340, 81%, 63%),
+ "purple": hsl(291, 53%, 63%),
+ "deep-purple": hsl(262, 73%, 70%),
+ "indigo": hsl(219, 76%, 62%),
+ "teal": hsl(174, 100%, 40%),
+ "green": hsl(122, 39%, 60%),
+ "deep-orange": hsl(14, 100%, 65%),
+ "brown": hsl(16, 45%, 56%),
+
+ // Set neutral colors to indigo
+ "grey": hsl(219, 66%, 62%),
+ "blue-grey": hsl(219, 66%, 62%),
+ "white": hsl(219, 66%, 62%),
+ "black": hsl(219, 66%, 62%)
+ ) {
+ [data-md-color-scheme="slate"][data-md-color-primary="#{$name}"] {
+ --md-typeset-a-color: #{$color};
+ }
+ }
+
+ // --------------------------------------------------------------------------
+
+ // Switching in progress - disable all transitions temporarily
+ [data-md-color-switching] *,
+ [data-md-color-switching] *::before,
+ [data-md-color-switching] *::after {
+ transition-duration: 0ms !important; // stylelint-disable-line
+ }
+}
diff --git a/src/templates/assets/stylesheets/utilities/_break.scss b/src/templates/assets/stylesheets/utilities/_break.scss
new file mode 100644
index 00000000..7ccd8622
--- /dev/null
+++ b/src/templates/assets/stylesheets/utilities/_break.scss
@@ -0,0 +1,219 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+@use "sass:list";
+@use "sass:map";
+@use "sass:math";
+
+// ----------------------------------------------------------------------------
+// Variables
+// ----------------------------------------------------------------------------
+
+///
+/// Device-specific breakpoints
+///
+/// @example
+/// $break-devices: (
+/// mobile: (
+/// portrait: 220px 479px,
+/// landscape: 480px 719px
+/// ),
+/// tablet: (
+/// portrait: 720px 959px,
+/// landscape: 960px 1219px
+/// ),
+/// screen: (
+/// small: 1220px 1599px,
+/// medium: 1600px 1999px,
+/// large: 2000px
+/// )
+/// );
+///
+$break-devices: () !default;
+
+// ----------------------------------------------------------------------------
+// Helpers
+// ----------------------------------------------------------------------------
+
+///
+/// Choose minimum and maximum device widths
+///
+@function break-select-min-max($devices) {
+ $min: 1000000;
+ $max: 0;
+ @each $key, $value in $devices {
+ @while type-of($value) == map {
+ $value: break-select-min-max($value);
+ }
+ @if type-of($value) == list {
+ @each $number in $value {
+ @if type-of($number) == number {
+ $min: math.min($number, $min);
+ @if $max {
+ $max: math.max($number, $max);
+ }
+ } @else {
+ @error "Invalid number: #{$number}";
+ }
+ }
+ } @else if type-of($value) == number {
+ $min: math.min($value, $min);
+ $max: null;
+ } @else {
+ @error "Invalid value: #{$value}";
+ }
+ }
+ @return $min, $max;
+}
+
+///
+/// Select minimum and maximum widths for a device breakpoint
+///
+@function break-select-device($device) {
+ $current: $break-devices;
+ @for $n from 1 through length($device) {
+ @if type-of($current) == map {
+ $current: map.get($current, list.nth($device, $n));
+ } @else {
+ @error "Invalid device map: #{$devices}";
+ }
+ }
+ @if type-of($current) == list or type-of($current) == number {
+ $current: (default: $current);
+ }
+ @return break-select-min-max($current);
+}
+
+// ----------------------------------------------------------------------------
+// Mixins
+// ----------------------------------------------------------------------------
+
+///
+/// A minimum-maximum media query breakpoint
+///
+@mixin break-at($breakpoint) {
+ @if type-of($breakpoint) == number {
+ @media screen and (min-width: $breakpoint) {
+ @content;
+ }
+ } @else if type-of($breakpoint) == list {
+ $min: list.nth($breakpoint, 1);
+ $max: list.nth($breakpoint, 2);
+ @if type-of($min) == number and type-of($max) == number {
+ @media screen and (min-width: $min) and (max-width: $max) {
+ @content;
+ }
+ } @else {
+ @error "Invalid breakpoint: #{$breakpoint}";
+ }
+ } @else {
+ @error "Invalid breakpoint: #{$breakpoint}";
+ }
+}
+
+///
+/// An orientation media query breakpoint
+///
+@mixin break-at-orientation($breakpoint) {
+ @if type-of($breakpoint) == string {
+ @media screen and (orientation: $breakpoint) {
+ @content;
+ }
+ } @else {
+ @error "Invalid breakpoint: #{$breakpoint}";
+ }
+}
+
+///
+/// A maximum-aspect-ratio media query breakpoint
+///
+@mixin break-at-ratio($breakpoint) {
+ @if type-of($breakpoint) == number {
+ @media screen and (max-aspect-ratio: $breakpoint) {
+ @content;
+ }
+ } @else {
+ @error "Invalid breakpoint: #{$breakpoint}";
+ }
+}
+
+///
+/// A minimum-maximum media query device breakpoint
+///
+@mixin break-at-device($device) {
+ @if type-of($device) == string {
+ $device: $device,;
+ }
+ @if type-of($device) == list {
+ $breakpoint: break-select-device($device);
+ @if list.nth($breakpoint, 2) {
+ $min: list.nth($breakpoint, 1);
+ $max: list.nth($breakpoint, 2);
+
+ @media screen and (min-width: $min) and (max-width: $max) {
+ @content;
+ }
+ } @else {
+ @error "Invalid device: #{$device}";
+ }
+ } @else {
+ @error "Invalid device: #{$device}";
+ }
+}
+
+///
+/// A minimum media query device breakpoint
+///
+@mixin break-from-device($device) {
+ @if type-of($device) == string {
+ $device: $device,;
+ }
+ @if type-of($device) == list {
+ $breakpoint: break-select-device($device);
+ $min: list.nth($breakpoint, 1);
+
+ @media screen and (min-width: $min) {
+ @content;
+ }
+ } @else {
+ @error "Invalid device: #{$device}";
+ }
+}
+
+///
+/// A maximum media query device breakpoint
+///
+@mixin break-to-device($device) {
+ @if type-of($device) == string {
+ $device: $device,;
+ }
+ @if type-of($device) == list {
+ $breakpoint: break-select-device($device);
+ $max: list.nth($breakpoint, 2);
+
+ @media screen and (max-width: $max) {
+ @content;
+ }
+ } @else {
+ @error "Invalid device: #{$device}";
+ }
+}
diff --git a/src/templates/assets/stylesheets/utilities/_convert.scss b/src/templates/assets/stylesheets/utilities/_convert.scss
new file mode 100644
index 00000000..8199c9c8
--- /dev/null
+++ b/src/templates/assets/stylesheets/utilities/_convert.scss
@@ -0,0 +1,79 @@
+////
+/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+///
+/// Permission is hereby granted, free of charge, to any person obtaining a
+/// copy of this software and associated documentation files (the "Software"),
+/// to deal in the Software without restriction, including without limitation
+/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+/// and/or sell copies of the Software, and to permit persons to whom the
+/// Software is furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+/// DEALINGS
+////
+
+@use "sass:math";
+
+// ----------------------------------------------------------------------------
+// Helpers
+// ----------------------------------------------------------------------------
+
+///
+/// Strip units from a number
+///
+@function strip-units($number) {
+ @return math.div($number, ($number * 0 + 1));
+}
+
+///
+/// Convert color in HEX to HSL
+///
+/// Note, that we need to strip the `deg` units from the `hue` value, as they
+/// were added in Color Level 4, which not all browsers support.
+///
+@function hex2hsl($color) {
+ @return
+ round(strip-units(hue($color))),
+ round(saturation($color)),
+ round(lightness($color));
+}
+
+// ----------------------------------------------------------------------------
+
+///
+/// Convert font size in px to em
+///
+@function px2em($size, $base: 16px) {
+ @if unit($size) == px {
+ @if unit($base) == px {
+ @return math.div($size, $base) * 1em;
+ } @else {
+ @error "Invalid base: #{$base} - unit must be 'px'";
+ }
+ } @else {
+ @error "Invalid size: #{$size} - unit must be 'px'";
+ }
+}
+
+///
+/// Convert font size in px to rem
+///
+@function px2rem($size, $base: 20px) {
+ @if unit($size) == px {
+ @if unit($base) == px {
+ @return math.div($size, $base) * 1rem;
+ } @else {
+ @error "Invalid base: #{$base} - unit must be 'px'";
+ }
+ } @else {
+ @error "Invalid size: #{$size} - unit must be 'px'";
+ }
+}
diff --git a/src/templates/base.html b/src/templates/base.html
new file mode 100644
index 00000000..8323e76e
--- /dev/null
+++ b/src/templates/base.html
@@ -0,0 +1,445 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+{% import "partials/language.html" as lang with context %}
+
+<!doctype html>
+<html lang="{{ lang.t('language') }}" class="no-js">
+ <head>
+
+ <!-- Meta tags -->
+ {% block site_meta %}
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+
+ <!-- Page description -->
+ {% if page.meta and page.meta.description %}
+ <meta name="description" content="{{ page.meta.description }}" />
+ {% elif config.site_description %}
+ <meta name="description" content="{{ config.site_description }}" />
+ {% endif %}
+
+ <!-- Page author -->
+ {% if page.meta and page.meta.author %}
+ <meta name="author" content="{{ page.meta.author }}" />
+ {% elif config.site_author %}
+ <meta name="author" content="{{ config.site_author }}" />
+ {% endif %}
+
+ <!-- Canonical -->
+ {% if page.canonical_url %}
+ <link rel="canonical" href="{{ page.canonical_url }}" />
+ {% endif %}
+
+ <!-- Previous page -->
+ {% if page.previous_page %}
+ <link rel="prev" href="{{ page.previous_page.url | url }}" />
+ {% endif %}
+
+ <!-- Next page -->
+ {% if page.next_page %}
+ <link rel="next" href="{{ page.next_page.url | url }}" />
+ {% endif %}
+
+ <!-- RSS feed -->
+ {% if "rss" in config.plugins %}
+ <link
+ rel="alternate"
+ type="application/rss+xml"
+ title="{{ lang.t('rss.created') }}"
+ href="{{ 'feed_rss_created.xml' | url }}"
+ />
+ <link
+ rel="alternate"
+ type="application/rss+xml"
+ title="{{ lang.t('rss.updated') }}"
+ href="{{ 'feed_rss_updated.xml' | url }}"
+ />
+ {% endif %}
+
+ <!-- Favicon -->
+ <link rel="icon" href="{{ config.theme.favicon | url }}" />
+
+ <!-- Generator banner -->
+ <meta
+ name="generator"
+ content="mkdocs-{{ mkdocs_version }}, $md-name$-$md-version$"
+ />
+ {% endblock %}
+
+ <!-- Site title -->
+ {% block htmltitle %}
+ {% if page.meta and page.meta.title %}
+ <title>{{ page.meta.title }} - {{ config.site_name }}</title>
+ {% elif page.title and not page.is_homepage %}
+ <title>{{ page.title | striptags }} - {{ config.site_name }}</title>
+ {% else %}
+ <title>{{ config.site_name }}</title>
+ {% endif %}
+ {% endblock %}
+
+ <!-- Theme-related style sheets -->
+ {% block styles %}
+ <link rel="stylesheet" href="{{ 'assets/stylesheets/main.css' | url }}" />
+
+ <!-- Extra color palette -->
+ {% if config.theme.palette %}
+ {% set palette = config.theme.palette %}
+ <link
+ rel="stylesheet"
+ href="{{ 'assets/stylesheets/palette.css' | url }}"
+ />
+ {% endif %}
+
+ <!-- Custom icons -->
+ {% include "partials/icons.html" %}
+ {% endblock %}
+
+ <!-- JavaScript libraries -->
+ {% block libs %}
+ {% for script in config.extra.polyfills %}
+ {{ script | script_tag }}
+ {% endfor %}
+ {% endblock %}
+
+ <!-- Webfonts -->
+ {% block fonts %}
+
+ <!-- Load fonts from Google -->
+ {% if config.theme.font != false %}
+ {% set text = config.theme.font.get("text", "Roboto") %}
+ {% set code = config.theme.font.get("code", "Roboto Mono") %}
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+ <link
+ rel="stylesheet"
+ href="https://fonts.googleapis.com/css?family={{
+ text | replace(' ', '+') + ':300,300i,400,400i,700,700i%7C' +
+ code | replace(' ', '+') + ':400,400i,700,700i'
+ }}&display=fallback"
+ />
+ <style>
+ :root {
+ --md-text-font: "{{ text }}";
+ --md-code-font: "{{ code }}";
+ }
+ </style>
+ {% endif %}
+ {% endblock %}
+
+ <!-- Custom style sheets -->
+ {% for path in config.extra_css %}
+ <link rel="stylesheet" href="{{ path | url }}" />
+ {% endfor %}
+
+ <!-- Helper functions for inline scripts -->
+ {% include "partials/javascripts/base.html" %}
+
+ <!-- Analytics -->
+ {% block analytics %}
+ {% include "partials/integrations/analytics.html" %}
+ {% endblock %}
+
+ <!-- Meta tags from front matter or plugins -->
+ {% if page.meta and page.meta.meta %}
+ {% for tag in page.meta.meta %}
+ <meta
+ {% for key, value in tag.items() %}
+ {{ key }}="{{value}}"
+ {% endfor %}
+ />
+ {% endfor %}
+ {% endif %}
+
+ <!-- Custom front matter -->
+ {% block extrahead %}{% endblock %}
+ </head>
+
+ <!-- Set text direction and color palette, if defined -->
+ {% set direction = config.theme.direction or lang.t("direction") %}
+ {% if config.theme.palette %}
+ {% set palette = config.theme.palette %}
+ {% if not palette is mapping %}
+ {% set palette = palette | first %}
+ {% endif %}
+ {% set scheme = palette.scheme | d("default", true) %}
+ {% set primary = palette.primary | d("indigo", true) %}
+ {% set accent = palette.accent | d("indigo", true) %}
+ <body
+ dir="{{ direction }}"
+ data-md-color-scheme="{{ scheme | replace(' ', '-') }}"
+ data-md-color-primary="{{ primary | replace(' ', '-') }}"
+ data-md-color-accent="{{ accent | replace(' ', '-') }}"
+ >
+ {% else %}
+ <body dir="{{ direction }}">
+ {% endif %}
+ {% set features = config.theme.features or [] %}
+
+ <!-- User preference: color palette -->
+ {% if not config.theme.palette is mapping %}
+ {% include "partials/javascripts/palette.html" %}
+ {% endif %}
+
+ <!--
+ State toggles - we need to set autocomplete="off" in order to reset the
+ drawer on back button invocation in some browsers
+ -->
+ <input
+ class="md-toggle"
+ data-md-toggle="drawer"
+ type="checkbox"
+ id="__drawer"
+ autocomplete="off"
+ />
+ <input
+ class="md-toggle"
+ data-md-toggle="search"
+ type="checkbox"
+ id="__search"
+ autocomplete="off"
+ />
+
+ <!-- Overlay for expanded drawer -->
+ <label class="md-overlay" for="__drawer"></label>
+
+ <!-- Skip to content -->
+ <div data-md-component="skip">
+ {% if page.toc | first is defined %}
+ {% set skip = page.toc | first %}
+ <a href="{{ skip.url | url }}" class="md-skip">
+ {{ lang.t("action.skip") }}
+ </a>
+ {% endif %}
+ </div>
+
+ <!-- Announcement bar -->
+ <div data-md-component="announce">
+ {% if self.announce() %}
+ <aside class="md-banner">
+ <div class="md-banner__inner md-grid md-typeset">
+
+ <!-- Button to dismiss announcement -->
+ {% if "announce.dismiss" in features %}
+ <button
+ class="md-banner__button md-icon"
+ aria-label="{{ lang.t('announce.dismiss') }}"
+ >
+ {% set icon = config.theme.icon.close or "material/close" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </button>
+ {% endif %}
+
+ <!-- Announcement bar content -->
+ {% block announce %}{% endblock %}
+ </div>
+ {% if "announce.dismiss" in features %}
+ {% include "partials/javascripts/announce.html" %}
+ {% endif %}
+ </aside>
+ {% endif %}
+ </div>
+
+ <!-- Version warning -->
+ {% if config.extra.version %}
+ <div data-md-color-scheme="default" data-md-component="outdated" hidden>
+ {% if self.outdated() %}
+ <aside class="md-banner md-banner--warning">
+ <div class="md-banner__inner md-grid md-typeset">
+ {% block outdated %}{% endblock %}
+ </div>
+ {% include "partials/javascripts/outdated.html" %}
+ </aside>
+ {% endif %}
+ </div>
+ {% endif %}
+
+ <!-- Header -->
+ {% block header %}
+ {% include "partials/header.html" %}
+ {% endblock %}
+
+ <!-- Container -->
+ <div class="md-container" data-md-component="container">
+
+ <!-- Hero teaser -->
+ {% block hero %}{% endblock %}
+
+ <!-- Navigation tabs (collapsing) -->
+ {% block tabs %}
+ {% if "navigation.tabs.sticky" not in features %}
+ {% if "navigation.tabs" in features %}
+ {% include "partials/tabs.html" %}
+ {% endif %}
+ {% endif %}
+ {% endblock %}
+
+ <!-- Main area -->
+ <main class="md-main" data-md-component="main">
+ <div class="md-main__inner md-grid">
+
+ <!-- Sidebars -->
+ {% block site_nav %}
+
+ <!-- Navigation -->
+ {% if nav %}
+ {% if page.meta and page.meta.hide %}
+ {% set hidden = "hidden" if "navigation" in page.meta.hide %}
+ {% endif %}
+ <div
+ class="md-sidebar md-sidebar--primary"
+ data-md-component="sidebar"
+ data-md-type="navigation"
+ {{ hidden }}
+ >
+ <div class="md-sidebar__scrollwrap">
+ <div class="md-sidebar__inner">
+ {% include "partials/nav.html" %}
+ </div>
+ </div>
+ </div>
+ {% endif %}
+
+ <!-- Table of contents -->
+ {% if "toc.integrate" not in features %}
+ {% if page.meta and page.meta.hide %}
+ {% set hidden = "hidden" if "toc" in page.meta.hide %}
+ {% endif %}
+ <div
+ class="md-sidebar md-sidebar--secondary"
+ data-md-component="sidebar"
+ data-md-type="toc"
+ {{ hidden }}
+ >
+ <div class="md-sidebar__scrollwrap">
+ <div class="md-sidebar__inner">
+ {% include "partials/toc.html" %}
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ {% endblock %}
+
+ <!-- Page content -->
+ {% block container %}
+ <div class="md-content" data-md-component="content">
+ <article class="md-content__inner md-typeset">
+ {% block content %}
+ {% include "partials/content.html" %}
+ {% endblock %}
+ </article>
+ </div>
+ {% endblock %}
+
+ <!-- User preference: content -->
+ {% include "partials/javascripts/content.html" %}
+ </div>
+
+ <!-- Back-to-top button -->
+ {% if "navigation.top" in features %}
+ {% include "partials/top.html" %}
+ {% endif %}
+ </main>
+
+ <!-- Footer -->
+ {% block footer %}
+ {% include "partials/footer.html" %}
+ {% endblock %}
+ </div>
+
+ <!-- Dialog -->
+ <div class="md-dialog" data-md-component="dialog">
+ <div class="md-dialog__inner md-typeset"></div>
+ </div>
+
+ <!-- Progress indicator -->
+ {% if "navigation.instant.progress" in features %}
+ {% include "partials/progress.html" %}
+ {% endif %}
+
+ <!-- Consent -->
+ {% if config.extra.consent %}
+ <div class="md-consent" data-md-component="consent" id="__consent" hidden>
+ <div class="md-consent__overlay"></div>
+ <aside class="md-consent__inner">
+ <form class="md-consent__form md-grid md-typeset" name="consent">
+ {% include "partials/consent.html" %}
+ </form>
+ </aside>
+ </div>
+
+ <!-- User preference: consent -->
+ {% include "partials/javascripts/consent.html" %}
+ {% endif %}
+
+ <!-- Theme-related configuration -->
+ {% block config %}
+ {%- set app = {
+ "base": base_url,
+ "features": features,
+ "translations": {},
+ "search": "assets/javascripts/workers/search.js" | url
+ } -%}
+
+ <!-- Versioning -->
+ {%- if config.extra.version -%}
+ {%- set _ = app.update({ "version": config.extra.version }) -%}
+ {%- endif -%}
+
+ <!-- Tags -->
+ {%- if config.extra.tags -%}
+ {%- set _ = app.update({ "tags": config.extra.tags }) -%}
+ {%- endif -%}
+
+ <!-- Translations -->
+ {%- set translations = app.translations -%}
+ {%- for key in [
+ "clipboard.copy",
+ "clipboard.copied",
+ "search.result.placeholder",
+ "search.result.none",
+ "search.result.one",
+ "search.result.other",
+ "search.result.more.one",
+ "search.result.more.other",
+ "search.result.term.missing",
+ "select.version"
+ ] -%}
+ {%- set _ = translations.update({ key: lang.t(key) }) -%}
+ {%- endfor -%}
+
+ <!-- Configuration -->
+ <script id="__config" type="application/json">
+ {{- app | tojson -}}
+ </script>
+ {% endblock %}
+
+ <!-- Theme-related JavaScript -->
+ {% block scripts %}
+ <script src="{{ 'assets/javascripts/bundle.js' | url }}"></script>
+
+ <!-- Custom JavaScript -->
+ {% for script in config.extra_javascript %}
+ {{ script | script_tag }}
+ {% endfor %}
+ {% endblock %}
+ </body>
+</html>
diff --git a/src/templates/blog-post.html b/src/templates/blog-post.html
new file mode 100644
index 00000000..73fb669f
--- /dev/null
+++ b/src/templates/blog-post.html
@@ -0,0 +1,164 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+{% extends "main.html" %}
+
+{% import "partials/nav-item.html" as item with context %}
+
+<!-- Page content -->
+{% block container %}
+ <div class="md-content md-content--post" data-md-component="content">
+
+ <!-- Sidebar -->
+ <div
+ class="md-sidebar md-sidebar--post"
+ data-md-component="sidebar"
+ data-md-type="navigation"
+ >
+ <div class="md-sidebar__scrollwrap">
+ <div class="md-sidebar__inner md-post">
+ <nav class="md-nav md-nav--primary">
+
+ <!-- Back to overview link -->
+ <div class="md-post__back">
+ <div class="md-nav__title md-nav__container">
+ <a href="{{ page.parent.url | url }}" class=" md-nav__link">
+ {% include ".icons/material/arrow-left.svg" %}
+ <span class="md-ellipsis">
+ {{ lang.t("blog.index") }}
+ </span>
+ </a>
+ </div>
+ </div>
+
+ <!-- Post authors -->
+ {% if page.authors %}
+ <div class="md-post__authors md-typeset">
+ {% for author in page.authors %}
+ <div class="md-profile md-post__profile">
+ <span class="md-author md-author--long">
+ <img src="{{ author.avatar }}" alt="{{ author.name }}" />
+ </span>
+ <span class="md-profile__description">
+ <strong>{{ author.name }}</strong><br />
+ {{ author.description }}
+ </span>
+ </div>
+ {% endfor %}
+ </div>
+ {% endif %}
+
+ <!-- Post metadata -->
+ <ul class="md-post__meta md-nav__list">
+ <li class="md-nav__item md-nav__item--section">
+ <div class="md-post__title">
+ <span class="md-ellipsis">
+ {{ lang.t("blog.meta") }}
+ </span>
+ </div>
+ <nav class="md-nav">
+ <ul class="md-nav__list">
+
+ <!-- Post date -->
+ <li class="md-nav__item">
+ <div class="md-nav__link">
+ {% include ".icons/material/calendar.svg" %}
+ <time
+ datetime="{{ page.config.date.created }}"
+ class="md-ellipsis"
+ >
+ {{- page.config.date.created | date -}}
+ </time>
+ </div>
+ </li>
+
+ <!-- Post date updated -->
+ {% if page.config.date.updated %}
+ <li class="md-nav__item">
+ <div class="md-nav__link">
+ {% include ".icons/material/calendar-clock.svg" %}
+ <time
+ datetime="{{ page.config.date.updated }}"
+ class="md-ellipsis"
+ >
+ {{- page.config.date.updated | date -}}
+ </time>
+ </div>
+ </li>
+ {% endif %}
+
+ <!-- Post categories -->
+ {% if page.categories %}
+ <li class="md-nav__item">
+ <div class="md-nav__link">
+ {% include ".icons/material/bookshelf.svg" %}
+ <span class="md-ellipsis">
+ {{ lang.t("blog.categories.in") }}
+ {% for category in page.categories %}
+ <a href="{{ category.url | url }}">
+ {{- category.title -}}
+ </a>
+ {%- if loop.revindex > 1 %}, {% endif -%}
+ {% endfor -%}
+ </span>
+ </div>
+ </li>
+ {% endif %}
+
+ <!-- Post readtime -->
+ {% if page.config.readtime %}
+ {% set time = page.config.readtime %}
+ <li class="md-nav__item">
+ <div class="md-nav__link">
+ {% include ".icons/material/clock-outline.svg" %}
+ <span class="md-ellipsis">
+ {% if time == 1 %}
+ {{ lang.t("readtime.one") }}
+ {% else %}
+ {{ lang.t("readtime.other") | replace("#", time) }}
+ {% endif %}
+ </span>
+ </div>
+ </li>
+ {% endif %}
+ </ul>
+ </nav>
+ </li>
+ </ul>
+ </nav>
+
+ <!-- Table of contents, if integrated -->
+ {% if "toc.integrate" in features %}
+ {% include "partials/toc.html" %}
+ {% endif %}
+ </div>
+ </div>
+ </div>
+
+ <!-- Page content -->
+ <article class="md-content__inner md-typeset">
+ {% block content %}
+ {% include "partials/content.html" %}
+ {% endblock %}
+ </article>
+ </div>
+{% endblock %}
diff --git a/src/templates/blog.html b/src/templates/blog.html
new file mode 100644
index 00000000..eedc77d9
--- /dev/null
+++ b/src/templates/blog.html
@@ -0,0 +1,48 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+{% extends "main.html" %}
+
+<!-- Page content -->
+{% block container %}
+ <div class="md-content" data-md-component="content">
+ <div class="md-content__inner">
+
+ <!-- Header -->
+ <header class="md-typeset">
+ {{ page.content }}
+ </header>
+
+ <!-- Posts -->
+ {% for post in posts %}
+ {% include "partials/post.html" %}
+ {% endfor %}
+
+ <!-- Pagination -->
+ {% if pagination %}
+ {% block pagination %}
+ {% include "partials/pagination.html" %}
+ {% endblock %}
+ {% endif %}
+ </div>
+ </div>
+{% endblock %}
diff --git a/src/templates/main.html b/src/templates/main.html
new file mode 100644
index 00000000..3b77d200
--- /dev/null
+++ b/src/templates/main.html
@@ -0,0 +1,23 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+{% extends "base.html" %}
diff --git a/src/templates/mkdocs_theme.yml b/src/templates/mkdocs_theme.yml
new file mode 100644
index 00000000..aaa47f5e
--- /dev/null
+++ b/src/templates/mkdocs_theme.yml
@@ -0,0 +1,50 @@
+# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# Language for theme localization
+language: en
+
+# Text direction (can be ltr or rtl), default: ltr
+direction:
+
+# Feature flags for functionality that alters behavior significantly, and thus
+# may be a matter of taste
+features: []
+
+# Fonts used by Material, automatically loaded from Google Fonts - see the site
+# for a list of available fonts
+font:
+
+ # Default font for text
+ text: Roboto
+
+ # Fixed-width font for code listings
+ code: Roboto Mono
+
+# From Material 5.x on, icons are inlined into the HTML and CSS as SVGs.
+# Icons that are part of the HTML can be configured and replaced
+icon:
+
+# Favicon to be rendered
+favicon: assets/images/favicon.png
+
+# Static pages to build
+static_templates:
+ - 404.html
diff --git a/src/templates/partials/actions.html b/src/templates/partials/actions.html
new file mode 100644
index 00000000..75fcb8eb
--- /dev/null
+++ b/src/templates/partials/actions.html
@@ -0,0 +1,54 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Actions -->
+{% if page.edit_url %}
+
+ <!-- Edit button -->
+ {% if "content.action.edit" in features %}
+ <a
+ href="{{ page.edit_url }}"
+ title="{{ lang.t('action.edit') }}"
+ class="md-content__button md-icon"
+ >
+ {% set icon = config.theme.icon.edit or "material/file-edit-outline" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </a>
+ {% endif %}
+
+ <!-- View button -->
+ {% if "content.action.view" in features %}
+ {% if "/blob/" in page.edit_url %}
+ {% set part = "blob" %}
+ {% else %}
+ {% set part = "edit" %}
+ {% endif %}
+ <a
+ href="{{ page.edit_url | replace(part, 'raw') }}"
+ title="{{ lang.t('action.view') }}"
+ class="md-content__button md-icon"
+ >
+ {% set icon = config.theme.icon.view or "material/file-eye-outline" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </a>
+ {% endif %}
+{% endif %}
diff --git a/src/templates/partials/alternate.html b/src/templates/partials/alternate.html
new file mode 100644
index 00000000..7d7c925b
--- /dev/null
+++ b/src/templates/partials/alternate.html
@@ -0,0 +1,49 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Site language selector -->
+<div class="md-header__option">
+ <div class="md-select">
+ {% set icon = config.theme.icon.alternate or "material/translate" %}
+ <button
+ class="md-header__button md-icon"
+ aria-label="{{ lang.t('select.language') }}"
+ >
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </button>
+ <div class="md-select__inner">
+ <ul class="md-select__list">
+ {% for alt in config.extra.alternate %}
+ <li class="md-select__item">
+ <a
+ href="{{ alt.link | url }}"
+ hreflang="{{ alt.lang }}"
+ class="md-select__link"
+ >
+ {{ alt.name }}
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+</div>
diff --git a/src/templates/partials/comments.html b/src/templates/partials/comments.html
new file mode 100644
index 00000000..6641d20e
--- /dev/null
+++ b/src/templates/partials/comments.html
@@ -0,0 +1,23 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Comment system -->
diff --git a/src/templates/partials/consent.html b/src/templates/partials/consent.html
new file mode 100644
index 00000000..c84622bc
--- /dev/null
+++ b/src/templates/partials/consent.html
@@ -0,0 +1,107 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Determine cookies -->
+{% set cookies = config.extra.consent.cookies | d({}) %}
+{% if config.extra.analytics %}
+ {% if "analytics" not in cookies %}
+ {% set _ = cookies.update({ "analytics": "Google Analytics" }) %}
+ {% endif %}
+{% endif %}
+{% if config.repo_url and "github.com" in config.repo_url %}
+ {% if "github" not in cookies %}
+ {% set _ = cookies.update({ "github": "GitHub" }) %}
+ {% endif %}
+{% endif %}
+
+<!-- Determine actions -->
+{% set actions = config.extra.consent.actions %}
+{% if not actions %}
+ {% set actions = ["accept", "manage"] %}
+{% endif %}
+
+<!-- Determine initial settings state -->
+{% if "manage" not in actions %}
+ {% set checked = "checked" %}
+{% endif %}
+
+<!-- Consent title -->
+<h4>{{ config.extra.consent.title }}</h4>
+<p>{{ config.extra.consent.description }}</p>
+
+<!-- Consent settings -->
+<input
+ class="md-toggle"
+ type="checkbox"
+ id="__settings"
+ {{ checked }}
+/>
+<div class="md-consent__settings">
+ <ul class="task-list">
+ {% for type in cookies %}
+ {% set checked = "" %}
+ {% if cookies[type] is string %}
+ {% set name = cookies[type] %}
+ {% set checked = "checked" %}
+ {% else %}
+ {% set name = cookies[type].name %}
+ {% if cookies[type].checked %}
+ {% set checked = "checked" %}
+ {% endif %}
+ {% endif %}
+ <li class="task-list-item">
+ <label class="task-list-control">
+ <input type="checkbox" name="{{ type }}" {{ checked }}>
+ <span class="task-list-indicator"></span>
+ {{ name }}
+ </label>
+ </li>
+ {% endfor %}
+ </ul>
+</div>
+
+<!-- Consent controls -->
+<div class="md-consent__controls">
+ {% for action in actions %}
+
+ <!-- Button to accept cookies -->
+ {% if action == "accept" %}
+ <button class="md-button md-button--primary">
+ {{- lang.t("consent.accept") -}}
+ </button>
+ {% endif %}
+
+ <!-- Button to reject cookies -->
+ {% if action == "reject" %}
+ <button type="reset" class="md-button md-button--primary">
+ {{- lang.t("consent.reject") -}}
+ </button>
+ {% endif %}
+
+ <!-- Button to manage settings -->
+ {% if action == "manage" %}
+ <label class="md-button" for="__settings">
+ {{- lang.t("consent.manage") -}}
+ </label>
+ {% endif %}
+ {% endfor %}
+</div>
diff --git a/src/templates/partials/content.html b/src/templates/partials/content.html
new file mode 100644
index 00000000..2b78b09b
--- /dev/null
+++ b/src/templates/partials/content.html
@@ -0,0 +1,54 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Tags -->
+{% if "material/tags" in config.plugins and tags %}
+ {% include "partials/tags.html" %}
+{% endif %}
+
+<!-- Actions -->
+{% include "partials/actions.html" %}
+
+<!--
+ Hack: check whether the content contains a h1 headline. If it doesn't, the
+ page title (or respectively site name) is used as the main headline.
+-->
+{% if "\x3ch1" not in page.content %}
+ <h1>{{ page.title | d(config.site_name, true)}}</h1>
+{% endif %}
+
+<!-- Page content -->
+{{ page.content }}
+
+<!-- Source file information -->
+{% if page.meta and (
+ page.meta.git_revision_date_localized or
+ page.meta.revision_date
+) %}
+ {% include "partials/source-file.html" %}
+{% endif %}
+
+<!-- Was this page helpful? -->
+{% include "partials/feedback.html" %}
+
+<!-- Comment system -->
+{% include "partials/comments.html" %}
diff --git a/src/templates/partials/copyright.html b/src/templates/partials/copyright.html
new file mode 100644
index 00000000..070948d2
--- /dev/null
+++ b/src/templates/partials/copyright.html
@@ -0,0 +1,39 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Copyright and theme information -->
+<div class="md-copyright">
+ {% if config.copyright %}
+ <div class="md-copyright__highlight">
+ {{ config.copyright }}
+ </div>
+ {% endif %}
+ {% if not config.extra.generator == false %}
+ Made with
+ <a
+ href="https://squidfunk.github.io/mkdocs-material/"
+ target="_blank" rel="noopener"
+ >
+ Material for MkDocs
+ </a>
+ {% endif %}
+</div>
diff --git a/src/templates/partials/feedback.html b/src/templates/partials/feedback.html
new file mode 100644
index 00000000..bf27c640
--- /dev/null
+++ b/src/templates/partials/feedback.html
@@ -0,0 +1,79 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Determine feedback configuration -->
+{% if config.extra.analytics %}
+ {% set feedback = config.extra.analytics.feedback %}
+{% endif %}
+
+<!-- Determine whether to show feedback -->
+{% if page.meta and page.meta.hide %}
+ {% if "feedback" in page.meta.hide %}
+ {% set feedback = None %}
+ {% endif %}
+{% endif %}
+
+<!-- Was this page helpful? -->
+{% if feedback %}
+ <form class="md-feedback" name="feedback" hidden>
+ <fieldset>
+ <legend class="md-feedback__title">
+ {{ feedback.title }}
+ </legend>
+ <div class="md-feedback__inner">
+
+ <!-- Feedback ratings -->
+ <div class="md-feedback__list">
+ {% for rating in feedback.ratings %}
+ <button
+ class="md-feedback__icon md-icon"
+ type="submit"
+ title="{{ rating.name }}"
+ data-md-value="{{ rating.data }}"
+ >
+ {% include ".icons/" ~ rating.icon ~ ".svg" %}
+ </button>
+ {% endfor %}
+ </div>
+
+ <!-- Feedback rating notes (shown after submission) -->
+ <div class="md-feedback__note">
+ {% for rating in feedback.ratings %}
+ <div data-md-value="{{ rating.data }}" hidden>
+ {% set url = "/" ~ page.url %}
+
+ <!-- Determine title -->
+ {% if page.meta and page.meta.title %}
+ {% set title = page.meta.title | urlencode %}
+ {% else %}
+ {% set title = page.title | urlencode %}
+ {% endif %}
+
+ <!-- Replace {url} and {title} placeholders in note -->
+ {{ rating.note.format(url = url, title = title) }}
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ </fieldset>
+ </form>
+{% endif %}
diff --git a/src/templates/partials/footer.html b/src/templates/partials/footer.html
new file mode 100644
index 00000000..ebe9278f
--- /dev/null
+++ b/src/templates/partials/footer.html
@@ -0,0 +1,98 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Footer -->
+<footer class="md-footer">
+
+ <!-- Link to previous and/or next page -->
+ {% if "navigation.footer" in features %}
+ {% if page.previous_page or page.next_page %}
+ {% if page.meta and page.meta.hide %}
+ {% set hidden = "hidden" if "footer" in page.meta.hide %}
+ {% endif %}
+ <nav
+ class="md-footer__inner md-grid"
+ aria-label="{{ lang.t('footer') }}"
+ {{ hidden }}
+ >
+
+ <!-- Link to previous page -->
+ {% if page.previous_page %}
+ {% set direction = lang.t("footer.previous") %}
+ <a
+ href="{{ page.previous_page.url | url }}"
+ class="md-footer__link md-footer__link--prev"
+ aria-label="{{ direction }}: {{ page.previous_page.title | e }}"
+ >
+ <div class="md-footer__button md-icon">
+ {% set icon = config.theme.icon.previous or "material/arrow-left" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </div>
+ <div class="md-footer__title">
+ <span class="md-footer__direction">
+ {{ direction }}
+ </span>
+ <div class="md-ellipsis">
+ {{ page.previous_page.title }}
+ </div>
+ </div>
+ </a>
+ {% endif %}
+
+ <!-- Link to next page -->
+ {% if page.next_page %}
+ {% set direction = lang.t("footer.next") %}
+ <a
+ href="{{ page.next_page.url | url }}"
+ class="md-footer__link md-footer__link--next"
+ aria-label="{{ direction }}: {{ page.next_page.title | e }}"
+ >
+ <div class="md-footer__title">
+ <span class="md-footer__direction">
+ {{ direction }}
+ </span>
+ <div class="md-ellipsis">
+ {{ page.next_page.title }}
+ </div>
+ </div>
+ <div class="md-footer__button md-icon">
+ {% set icon = config.theme.icon.next or "material/arrow-right" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </div>
+ </a>
+ {% endif %}
+ </nav>
+ {% endif %}
+ {% endif %}
+
+ <!-- Further information -->
+ <div class="md-footer-meta md-typeset">
+ <div class="md-footer-meta__inner md-grid">
+ {% include "partials/copyright.html" %}
+
+ <!-- Social links -->
+ {% if config.extra.social %}
+ {% include "partials/social.html" %}
+ {% endif %}
+ </div>
+ </div>
+</footer>
diff --git a/src/templates/partials/header.html b/src/templates/partials/header.html
new file mode 100644
index 00000000..9b6d2e2e
--- /dev/null
+++ b/src/templates/partials/header.html
@@ -0,0 +1,112 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Determine classes -->
+{% set class = "md-header" %}
+{% if "navigation.tabs.sticky" in features %}
+ {% set class = class ~ " md-header--shadow md-header--lifted" %}
+{% elif "navigation.tabs" not in features %}
+ {% set class = class ~ " md-header--shadow" %}
+{% endif %}
+
+<!-- Header -->
+<header class="{{ class }}" data-md-component="header">
+ <nav
+ class="md-header__inner md-grid"
+ aria-label="{{ lang.t('header') }}"
+ >
+
+ <!-- Link to home -->
+ <a
+ href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}"
+ title="{{ config.site_name | e }}"
+ class="md-header__button md-logo"
+ aria-label="{{ config.site_name }}"
+ data-md-component="logo"
+ >
+ {% include "partials/logo.html" %}
+ </a>
+
+ <!-- Button to open drawer -->
+ <label class="md-header__button md-icon" for="__drawer">
+ {% set icon = config.theme.icon.menu or "material/menu" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </label>
+
+ <!-- Header title -->
+ <div class="md-header__title" data-md-component="header-title">
+ <div class="md-header__ellipsis">
+ <div class="md-header__topic">
+ <span class="md-ellipsis">
+ {{ config.site_name }}
+ </span>
+ </div>
+ <div class="md-header__topic" data-md-component="header-topic">
+ <span class="md-ellipsis">
+ {% if page.meta and page.meta.title %}
+ {{ page.meta.title }}
+ {% else %}
+ {{ page.title }}
+ {% endif %}
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Color palette toggle -->
+ {% if config.theme.palette %}
+ {% if not config.theme.palette is mapping %}
+ {% include "partials/palette.html" %}
+ {% endif %}
+ {% endif %}
+
+ <!-- Site language selector -->
+ {% if config.extra.alternate %}
+ {% include "partials/alternate.html" %}
+ {% endif %}
+
+ <!-- Button to open search modal -->
+ {% if "material/search" in config.plugins %}
+ <label class="md-header__button md-icon" for="__search">
+ {% set icon = config.theme.icon.search or "material/magnify" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </label>
+
+ <!-- Search interface -->
+ {% include "partials/search.html" %}
+ {% endif %}
+
+ <!-- Repository information -->
+ {% if config.repo_url %}
+ <div class="md-header__source">
+ {% include "partials/source.html" %}
+ </div>
+ {% endif %}
+ </nav>
+
+ <!-- Navigation tabs (sticky) -->
+ {% if "navigation.tabs.sticky" in features %}
+ {% if "navigation.tabs" in features %}
+ {% include "partials/tabs.html" %}
+ {% endif %}
+ {% endif %}
+</header>
diff --git a/src/templates/partials/icons.html b/src/templates/partials/icons.html
new file mode 100644
index 00000000..17dd20d8
--- /dev/null
+++ b/src/templates/partials/icons.html
@@ -0,0 +1,72 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Custom admonition icons -->
+{% if config.theme.icon.admonition %}
+ {% set style = ["\x3cstyle\x3e:root{"] %}
+ {% for type, icon in config.theme.icon.admonition.items() %}
+ {% import ".icons/" ~ icon ~ ".svg" as icon %}
+ {% set _ = style.append(
+ "--md-admonition-icon--" ~ type ~ ":" ~
+ "url('data:image/svg+xml;charset=utf-8," ~
+ icon | replace("\n", "") ~
+ "');"
+ ) %}
+ {% endfor %}
+ {% set _ = style.append("}\x3c/style\x3e") %}
+ {{ style | join }}
+{% endif %}
+
+<!-- Custom annotation icon -->
+{% if config.theme.icon.annotation %}
+ {% set style = ["\x3cstyle\x3e:root{"] %}
+ {% import ".icons/" ~ config.theme.icon.annotation ~ ".svg" as icon %}
+ {% set _ = style.append(
+ "--md-annotation-icon:" ~
+ "url('data:image/svg+xml;charset=utf-8," ~
+ icon | replace("\n", "") ~
+ "');"
+ ) %}
+ {% set _ = style.append("}\x3c/style\x3e") %}
+ {{ style | join }}
+{% endif %}
+
+<!-- Custom tag icons -->
+{% if config.theme.icon.tag %}
+ {% set style = ["\x3cstyle\x3e"] %}
+ {% for type, icon in config.theme.icon.tag.items() %}
+ {% import ".icons/" ~ icon ~ ".svg" as icon %}
+ {% if type != "default" %}
+ {% set modifier = "--" ~ type %}
+ {% endif %}
+ {% set _ = style.append(
+ ".md-tag" ~ modifier ~ "{" ~
+ "--md-tag-icon:" ~
+ "url('data:image/svg+xml;charset=utf-8," ~
+ icon | replace("\n", "") ~
+ "');" ~
+ "}"
+ ) %}
+ {% endfor %}
+ {% set _ = style.append("\x3c/style\x3e") %}
+ {{ style | join }}
+{% endif %}
diff --git a/src/templates/partials/integrations/analytics.html b/src/templates/partials/integrations/analytics.html
new file mode 100644
index 00000000..4b483046
--- /dev/null
+++ b/src/templates/partials/integrations/analytics.html
@@ -0,0 +1,49 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Determine analytics provider -->
+{% if config.extra.analytics %}
+ {% set provider = config.extra.analytics.provider %}
+{% endif %}
+
+<!-- Set up analytics provider -->
+{% if provider %}
+ {% include "partials/integrations/analytics/" ~ provider ~ ".html" %}
+
+ <!-- Consent necessary -->
+ {% if config.extra.consent %}
+ <script>
+ if (typeof __md_analytics !== "undefined") {
+ var consent = __md_get("__consent")
+ if (consent && consent.analytics)
+ __md_analytics()
+ }
+ </script>
+
+ <!-- Consent unnecessary -->
+ {% else %}
+ <script>
+ if (typeof __md_analytics !== "undefined")
+ __md_analytics()
+ </script>
+ {% endif %}
+{% endif %}
diff --git a/src/templates/partials/integrations/analytics/google.html b/src/templates/partials/integrations/analytics/google.html
new file mode 100644
index 00000000..a9fa37d9
--- /dev/null
+++ b/src/templates/partials/integrations/analytics/google.html
@@ -0,0 +1,97 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Determine analytics property -->
+{% if config.extra.analytics %}
+ {% set property = config.extra.analytics.property | d("", true) %}
+{% endif %}
+
+<!-- Integrate with Google Analytics 4 -->
+<script id="__analytics">
+ function __md_analytics() {
+ window.dataLayer = window.dataLayer || []
+ function gtag() { dataLayer.push(arguments) }
+
+ /* Set up integration and send page view */
+ gtag("js", new Date())
+ gtag("config", "{{ property }}")
+
+ /* Register event handlers after documented loaded */
+ document.addEventListener("DOMContentLoaded", function() {
+
+ /* Set up search tracking */
+ if (document.forms.search) {
+ var query = document.forms.search.query
+ query.addEventListener("blur", function() {
+ if (this.value)
+ gtag("event", "search", { search_term: this.value })
+ })
+ }
+
+ /* Set up feedback, i.e. "Was this page helpful?" */
+ document$.subscribe(function() {
+ var feedback = document.forms.feedback
+ if (typeof feedback === "undefined")
+ return
+
+ /* Send feedback to Google Analytics */
+ for (var button of feedback.querySelectorAll("[type=submit]")) {
+ button.addEventListener("click", function(ev) {
+ ev.preventDefault()
+
+ /* Retrieve and send data */
+ var page = document.location.pathname
+ var data = this.getAttribute("data-md-value")
+ gtag("event", "feedback", { page, data })
+
+ /* Disable form and show note, if given */
+ feedback.firstElementChild.disabled = true
+ var note = feedback.querySelector(
+ ".md-feedback__note [data-md-value='" + data + "']"
+ )
+ if (note)
+ note.hidden = false
+ })
+
+ /* Show feedback */
+ feedback.hidden = false
+ }
+ })
+
+ /* Send page view on location change */
+ location$.subscribe(function(url) {
+ gtag("config", "{{ property }}", {
+ page_path: url.pathname
+ })
+ })
+ })
+
+ /* Create script tag */
+ var script = document.createElement("script")
+ script.async = true
+ script.src = "https://www.googletagmanager.com/gtag/js?id={{ property }}"
+
+ /* Inject script tag */
+ var container = document.getElementById("__analytics")
+ container.insertAdjacentElement("afterEnd", script)
+ }
+</script>
diff --git a/src/templates/partials/javascripts/announce.html b/src/templates/partials/javascripts/announce.html
new file mode 100644
index 00000000..f13961b2
--- /dev/null
+++ b/src/templates/partials/javascripts/announce.html
@@ -0,0 +1,31 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Announcement bar -->
+<script>
+ var el = document.querySelector("[data-md-component=announce]")
+ if (el) {
+ var content = el.querySelector(".md-typeset")
+ if (__md_hash(content.innerHTML) === __md_get("__announce"))
+ el.hidden = true
+ }
+</script>
diff --git a/src/templates/partials/javascripts/base.html b/src/templates/partials/javascripts/base.html
new file mode 100644
index 00000000..f0eeeb8a
--- /dev/null
+++ b/src/templates/partials/javascripts/base.html
@@ -0,0 +1,48 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!--
+ A collection of functions used from within some partials to allow the usage
+ of state saved in local or session storage, e.g. to model preferences.
+-->
+<script>
+
+ /* Compute base path once to integrate with instant loading */
+ __md_scope = new URL("{{ config.extra.scope | d(base_url) }}", location)
+
+ /* Compute hash from the given string - see https://bit.ly/3pvPjXG */
+ __md_hash = v => [...v].reduce((h, c) => (h << 5) - h + c.charCodeAt(0), 0)
+
+ /* Fetch the value for a key from the given storage */
+ __md_get = (key, storage = localStorage, scope = __md_scope) => (
+ JSON.parse(storage.getItem(scope.pathname + "." + key))
+ )
+
+ /* Persist a key-value pair in the given storage */
+ __md_set = (key, value, storage = localStorage, scope = __md_scope) => {
+ try {
+ storage.setItem(scope.pathname + "." + key, JSON.stringify(value))
+ } catch (err) {
+ /* Uncritical, just swallow */
+ }
+ }
+</script>
diff --git a/src/templates/partials/javascripts/consent.html b/src/templates/partials/javascripts/consent.html
new file mode 100644
index 00000000..13730da7
--- /dev/null
+++ b/src/templates/partials/javascripts/consent.html
@@ -0,0 +1,61 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- User-preference: consent -->
+<script>
+ var consent = __md_get("__consent")
+ if (consent) {
+ for (var input of document.forms.consent.elements)
+ if (input.name)
+ input.checked = consent[input.name] || false
+
+ /* Show consent with a small delay, but not if browsing locally */
+ } else if (location.protocol !== "file:") {
+ setTimeout(function() {
+ var el = document.querySelector("[data-md-component=consent]")
+ el.hidden = false
+ }, 250)
+ }
+
+ /* Intercept submission of consent form */
+ var form = document.forms.consent
+ for (var action of ["submit", "reset"])
+ form.addEventListener(action, function(ev) {
+ ev.preventDefault()
+
+ /* Reject all cookies */
+ if (ev.type === "reset")
+ for (var input of document.forms.consent.elements)
+ if (input.name)
+ input.checked = false
+
+ /* Grab and serialize form data */
+ __md_set("__consent", Object.fromEntries(
+ Array.from(new FormData(form).keys())
+ .map(function(key) { return [key, true] })
+ ))
+
+ /* Remove anchor to omit consent from reappearing and reload */
+ location.hash = '';
+ location.reload()
+ })
+</script>
diff --git a/src/templates/partials/javascripts/content.html b/src/templates/partials/javascripts/content.html
new file mode 100644
index 00000000..d361f18b
--- /dev/null
+++ b/src/templates/partials/javascripts/content.html
@@ -0,0 +1,39 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- User-preference: link content tabs -->
+{% if "content.tabs.link" in features %}
+ <script>
+ var tabs = __md_get("__tabs")
+ if (Array.isArray(tabs))
+ main: for (var set of document.querySelectorAll(".tabbed-set")) {
+ var labels = set.querySelector(".tabbed-labels")
+ for (var tab of tabs)
+ for (var label of labels.getElementsByTagName("label"))
+ if (label.innerText.trim() === tab) {
+ var input = document.getElementById(label.htmlFor)
+ input.checked = true
+ continue main
+ }
+ }
+ </script>
+{% endif %}
diff --git a/src/templates/partials/javascripts/outdated.html b/src/templates/partials/javascripts/outdated.html
new file mode 100644
index 00000000..576f3c85
--- /dev/null
+++ b/src/templates/partials/javascripts/outdated.html
@@ -0,0 +1,29 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Version warning -->
+<script>
+ var el = document.querySelector("[data-md-component=outdated]")
+ var outdated = __md_get("__outdated", sessionStorage)
+ if (outdated === true && el)
+ el.hidden = false
+</script>
diff --git a/src/templates/partials/javascripts/palette.html b/src/templates/partials/javascripts/palette.html
new file mode 100644
index 00000000..a2daef1d
--- /dev/null
+++ b/src/templates/partials/javascripts/palette.html
@@ -0,0 +1,29 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- User preference: color palette -->
+<script>
+ var palette = __md_get("__palette")
+ if (palette && typeof palette.color === "object")
+ for (var key of Object.keys(palette.color))
+ document.body.setAttribute("data-md-color-" + key, palette.color[key])
+</script>
diff --git a/src/templates/partials/language.html b/src/templates/partials/language.html
new file mode 100644
index 00000000..e37b953b
--- /dev/null
+++ b/src/templates/partials/language.html
@@ -0,0 +1,28 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Import translations for given language and fallback -->
+{% import "partials/languages/" ~ config.theme.language ~ ".html" as lang %}
+{% import "partials/languages/en.html" as fallback %}
+
+<!-- Re-export translations -->
+{% macro t(key) %}{{ lang.t(key) or fallback.t(key) or key }}{% endmacro %}
diff --git a/src/templates/partials/languages/af.html b/src/templates/partials/languages/af.html
new file mode 100644
index 00000000..b7f9f8fa
--- /dev/null
+++ b/src/templates/partials/languages/af.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Afrikaans -->
+{% macro t(key) %}{{ {
+ "language": "af",
+ "action.edit": "Wysig hierdie bladsy",
+ "action.skip": "Slaan oor na inhoud",
+ "action.view": "Bekyk bron van hierdie bladsy",
+ "announce.dismiss": "Moenie dit weer wys nie",
+ "blog.archive": "Argief",
+ "blog.categories": "Kategorieë",
+ "blog.categories.in": "binne",
+ "blog.continue": "Lees verder",
+ "blog.draft": "Konsep",
+ "blog.index": "Terug na indeks",
+ "blog.meta": "Metadata",
+ "blog.references": "Verwante skakels",
+ "clipboard.copy": "Kopieer na knipbord",
+ "clipboard.copied": "gekopieer na knipbord",
+ "consent.accept": "Aanvaar",
+ "consent.manage": "Bestuur instellings",
+ "consent.reject": "Verwerp",
+ "footer": "Voetskrif",
+ "footer.next": "Volgende",
+ "footer.previous": "Vorige",
+ "header": "Kopskrif",
+ "meta.comments": "Kommentaar",
+ "meta.source": "Bron",
+ "nav": "Navigasie",
+ "readtime.one": "1 minuut se lees",
+ "readtime.other": "# minuut se lees",
+ "rss.created": "RSS-voer geskep",
+ "rss.updated": "RSS-voer van opgedateerde inhoud",
+ "search": "Soek",
+ "search.config.lang": "nl",
+ "search.placeholder": "Soek",
+ "search.share": "Deel",
+ "search.reset": "Terugstel",
+ "search.result.initializer": "Inisialisering van soektog",
+ "search.result.placeholder": "Tik om te begin soek",
+ "search.result.none": "Geen ooreenstemmende dokumente",
+ "search.result.one": "1 ooreenstemmende dokument",
+ "search.result.other": "# ooreenstemmende dokumente",
+ "search.result.more.one": "1 meer op hierdie bladsy",
+ "search.result.more.other": "# meer op hierdie bladsy",
+ "search.result.term.missing": "Vermis",
+ "select.language": "Kies taal",
+ "select.version": "Kies weergawe",
+ "source": "Slaan oor na inhoud",
+ "source.file.contributors": "Medewerkers",
+ "source.file.date.created": "Geskep",
+ "source.file.date.updated": "Laaste opdatering",
+ "tabs": "Duimgids",
+ "toc": "Inhoudsopgawe",
+ "top": "Terug na bo"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ar.html b/src/templates/partials/languages/ar.html
new file mode 100644
index 00000000..4d5da33a
--- /dev/null
+++ b/src/templates/partials/languages/ar.html
@@ -0,0 +1,77 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Arabic -->
+{% macro t(key) %}{{ {
+ "language": "ar",
+ "direction": "rtl",
+ "action.edit": "عدل الصفحة",
+ "action.skip": "انتقل إلى المحتوى",
+ "action.view": "عرض مصدر هذه الصفحة",
+ "announce.dismiss": "لا تظهر هذا مرة أخرى",
+ "blog.archive": "أرشيف",
+ "blog.categories": "فئات",
+ "blog.categories.in": "ضمن",
+ "blog.continue": "أكمل القراءة",
+ "blog.draft": "مسودة",
+ "blog.index": "رجوع إلى الفهرس",
+ "blog.meta": "بيانات وصفية",
+ "blog.references": "روابط ذات علاقة",
+ "clipboard.copy": "نسخ إلى الحافظة",
+ "clipboard.copied": "تم النسخ الى الحافظة",
+ "consent.accept": "قبول",
+ "consent.manage": "إدارة الإعدادات",
+ "consent.reject": "رفض",
+ "footer": "هامش سفلي",
+ "footer.next": "التالية",
+ "footer.previous": "السابقة",
+ "header": "عنوان العارضة",
+ "meta.comments": "التعليقات",
+ "meta.source": "المصدر",
+ "nav": "تصفح",
+ "readtime.one": "قراءة لمدة دقيقة",
+ "readtime.other": "دقائق قراءة #",
+ "rss.created": "ملقم بالخلاصات",
+ "rss.updated": "ملقم بالخلاصات المحدثة",
+ "search": "إبحث",
+ "search.config.pipeline": " ",
+ "search.placeholder": "بحث",
+ "search.share": "شارك",
+ "search.reset": "مسح كلي",
+ "search.result.initializer": "بدء البحث",
+ "search.result.placeholder": "اكتب لبدء البحث",
+ "search.result.none": "لا توجد نتائج",
+ "search.result.one": "نتائج البحث مستند واحد",
+ "search.result.other": "نتائج البحث # مستندات",
+ "search.result.more.one": "أكثر من 1 في هذه الصفحة",
+ "search.result.more.other": "أكثر من # في هذه الصفحة",
+ "search.result.term.missing": "مفقود",
+ "select.language": "إختر اللغة",
+ "select.version": "إختر الإصدار",
+ "source": "اذهب إلى المصدر",
+ "source.file.contributors": "المساهمون",
+ "source.file.date.created": "خلقت",
+ "source.file.date.updated": "اخر تحديث",
+ "tabs": "نوافذ",
+ "toc": "جدول المحتويات",
+ "top": "عد إلى الأعلى"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/be.html b/src/templates/partials/languages/be.html
new file mode 100644
index 00000000..c36c8402
--- /dev/null
+++ b/src/templates/partials/languages/be.html
@@ -0,0 +1,77 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Belarusian -->
+{% macro t(key) %}{{ {
+ "language": "be",
+ "direction": "ltr",
+ "action.edit": "Правіць старонку",
+ "action.skip": "Перайсці да зместа",
+ "action.view": "Паглядзець зыходны код старонкі",
+ "announce.dismiss": "Больш не паказваць",
+ "blog.archive": "Заархіваваць",
+ "blog.categories": "Катэгорыі",
+ "blog.categories.in": "у",
+ "blog.continue": "Працягнуць чытаць",
+ "blog.draft": "Чарнавік",
+ "blog.index": "Вярнуцца на хатнюю",
+ "blog.meta": "Метаданыя",
+ "blog.references": "Спасылкі па тэме",
+ "clipboard.copy": "Скапіраваць у буфер абмена",
+ "clipboard.copied": "Скапіравана ў буфер абмена",
+ "consent.accept": "Прыняць",
+ "consent.manage": "Кіраваць наладамі",
+ "consent.reject": "Адхіліць",
+ "footer": "Ніжні калантытул",
+ "footer.next": "Наступная",
+ "footer.previous": "Папярэдняя",
+ "header": "Верхні калантытул",
+ "meta.comments": "Каментарыі",
+ "meta.source": "Зыходны код",
+ "nav": "Навігацыя",
+ "readtime.one": "Прачытанне зойме 1 хв",
+ "readtime.other": "Прачытанне зойме # хв",
+ "rss.created": "RSS стужка",
+ "rss.updated": "RSS стужка з абноўленым зместам",
+ "search": "Пошук",
+ "search.config.lang": "ru",
+ "search.placeholder": "Пошук",
+ "search.share": "Падзяліцца",
+ "search.reset": "Ачысціць",
+ "search.result.initializer": "Пачынаем пошук",
+ "search.result.placeholder": "Пачніце друкаваць для пошуку",
+ "search.result.none": "Нічога ня знойдзена",
+ "search.result.one": "Адзін адпаведны дакумент",
+ "search.result.other": "Адпаведных дакументаў: #",
+ "search.result.more.one": "Яшчэ 1 на гэтай старонцы",
+ "search.result.more.other": "Яшчэ # на гэтай старонцы",
+ "search.result.term.missing": "Адсутнічае",
+ "select.language": "Выберыце мову",
+ "select.version": "Выберыце версію",
+ "source": "Перайсці ў рэпазітар",
+ "source.file.contributors": "Укладальнікі",
+ "source.file.date.created": "Створана",
+ "source.file.date.updated": "Апошняе абнаўленне",
+ "tabs": "Укладкі",
+ "toc": "Змест",
+ "top": "Вярнуцца да пачатку"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/bg.html b/src/templates/partials/languages/bg.html
new file mode 100644
index 00000000..f36fd437
--- /dev/null
+++ b/src/templates/partials/languages/bg.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Bulgarian -->
+{% macro t(key) %}{{ {
+ "language": "bg",
+ "action.edit": "Редактирай тази страница",
+ "action.skip": "Към съдържанието",
+ "action.view": "Виж съдържанието на тази страница",
+ "announce.dismiss": "Не показвай повече",
+ "blog.archive": "Архив",
+ "blog.categories": "Категории",
+ "blog.categories.in": "В",
+ "blog.continue": "Продължи четенето",
+ "blog.draft": "Чернова",
+ "blog.index": "Назад към индекса",
+ "blog.meta": "Метаданни",
+ "blog.references": "Свързани линкове",
+ "clipboard.copy": "Копирай",
+ "clipboard.copied": "Копирано",
+ "consent.accept": "Приеми",
+ "consent.manage": "Управление на настойките",
+ "consent.reject": "Откажи",
+ "footer": "Долен колонтитул",
+ "footer.next": "Следваща",
+ "footer.previous": "Предишна",
+ "header": "Горен колонтитул",
+ "meta.comments": "Коментари",
+ "meta.source": "Код",
+ "nav": "Навигация",
+ "readtime.one": "1 мин четено",
+ "readtime.other": "# мин четено",
+ "rss.created": "RSS публикации",
+ "rss.updated": "RSS публикации с актуализирано съдържание",
+ "search": "Търси",
+ "search.config.lang": "ru",
+ "search.placeholder": "Търси",
+ "search.share": "Сподели",
+ "search.reset": "Изчисти",
+ "search.result.initializer": "Инициализирано търсене",
+ "search.result.placeholder": "Започнете да пишете, за да търсите",
+ "search.result.none": "Няма резултати",
+ "search.result.one": "1 резултат",
+ "search.result.other": "# резултата",
+ "search.result.more.one": "още 1 на тази страница",
+ "search.result.more.other": "още # на тази страница",
+ "search.result.term.missing": "Липсващо",
+ "select.language": "Избери език",
+ "select.version": "Избери версия",
+ "source": "Към хранилището",
+ "source.file.contributors": "Участници",
+ "source.file.date.created": "Създаден",
+ "source.file.date.updated": "Последна промяна",
+ "tabs": "Табове",
+ "toc": "Съдържание",
+ "top": "Върни се в началото"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/bn.html b/src/templates/partials/languages/bn.html
new file mode 100644
index 00000000..0a3ee6d0
--- /dev/null
+++ b/src/templates/partials/languages/bn.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Bengali (Bangla) -->
+{% macro t(key) %}{{ {
+ "language": "bn",
+ "action.edit": "এই পেজ এডিট করুন",
+ "action.skip": "কনটেন্টে যান",
+ "action.view": "পেজের ভিউ",
+ "announce.dismiss": "আর কখনো দেখাবে না",
+ "blog.archive": "সংরক্ষণাগার",
+ "blog.categories": "বিভাগ",
+ "blog.categories.in": "বিভাগের মধ্যে",
+ "blog.continue": "পড়তে থাকুন",
+ "blog.draft": "খসড়া",
+ "blog.index": "ইনডেক্সে ফিরে যান",
+ "blog.meta": "মেটাডেটা",
+ "blog.references": "সম্পর্কিত লিংক",
+ "clipboard.copy": "ক্লিপবোর্ডে কপি করুন",
+ "clipboard.copied": "ক্লিপবোর্ডে কপি হয়েছে",
+ "consent.accept": "গ্রহণ",
+ "consent.manage": "সেটিংস ব্যবস্থাপনা",
+ "consent.reject": "প্রত্যাখ্যান",
+ "footer": "ফুটার",
+ "footer.next": "পরে",
+ "footer.previous": "পূর্ববর্তী",
+ "header": "হেডার",
+ "meta.comments": "মন্তব্য",
+ "meta.source": "উৎস",
+ "nav": "ন্যাভিগেশন",
+ "readtime.one": "১ মিনিট পড়া",
+ "readtime.other": "# মিনিট পড়া",
+ "rss.created": "আরএসএস ফিড",
+ "rss.updated": "আপডেট করা বিষয়বস্তুর আরএসএস ফিড",
+ "search": "অনুসন্ধান করুন",
+ "search.config.pipeline": " ",
+ "search.placeholder": "অনুসন্ধান করুন",
+ "search.share": "শেয়ার",
+ "search.reset": "রিসেট",
+ "search.result.initializer": "অনুসন্ধান শুরু করা হচ্ছে",
+ "search.result.placeholder": "সার্চ টাইপ করুন",
+ "search.result.none": "কিছু পাওয়া যায়নি",
+ "search.result.one": "১ টা ডকুমেন্ট",
+ "search.result.other": "# টা ডকুমেন্ট",
+ "search.result.more.one": "এই পৃষ্ঠায় আরও ১টি আছে",
+ "search.result.more.other": "এই পৃষ্ঠায় আরও #টি আছে",
+ "search.result.term.missing": "অনুপস্থিত",
+ "select.language": "ভাষা নির্বাচন করুণ",
+ "select.version": "সংস্করণ নির্বাচন করুণ",
+ "source": "রিপোজিটরিতে যান",
+ "source.file.contributors": "অবদানকারী",
+ "source.file.date.created": "তৈরি হয়েছে",
+ "source.file.date.updated": "শেষ আপডেট",
+ "tabs": "ট্যাব",
+ "toc": "সূচি তালিকা",
+ "top": "উপরে ফিরে যাও"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ca.html b/src/templates/partials/languages/ca.html
new file mode 100644
index 00000000..8fd2b03a
--- /dev/null
+++ b/src/templates/partials/languages/ca.html
@@ -0,0 +1,75 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Catalan -->
+{% macro t(key) %}{{ {
+ "language": "ca",
+ "action.edit": "Edita aquesta pàgina",
+ "action.skip": "Salta el contingut",
+ "action.view": "Visualitza el codi font",
+ "announce.dismiss": "No ho tornis a mostrar",
+ "blog.archive": "Arxiva",
+ "blog.categories": "Categories",
+ "blog.categories.in": "a",
+ "blog.continue": "Continua llegint",
+ "blog.draft": "Esborrany",
+ "blog.index": "Torna a l'índex",
+ "blog.meta": "Metadades",
+ "blog.references": "Enllaços relacionats",
+ "clipboard.copy": "Còpia al porta-retalls",
+ "clipboard.copied": "Copiat al porta-retalls",
+ "consent.accept": "Accepta",
+ "consent.manage": "Gestiona la configuració",
+ "consent.reject": "Rebutja",
+ "footer": "Peu de pàgina",
+ "footer.next": "Següent",
+ "footer.previous": "Anterior",
+ "header": "Capçalera",
+ "meta.comments": "Comentaris",
+ "meta.source": "Codi font",
+ "nav": "Navegació",
+ "readtime.one": "1 min de lectura",
+ "readtime.other": "# min de lectura",
+ "rss.created": "Canal RSS",
+ "rss.updated": "Canal RSS de contingut actualitzat",
+ "search": "Cerca",
+ "search.placeholder": "Cerca",
+ "search.share": "Comparteix",
+ "search.reset": "Neteja",
+ "search.result.initializer": "Inicialitzant cerca",
+ "search.result.placeholder": "Escriu per a començar a cercar",
+ "search.result.none": "Cap document coincideix",
+ "search.result.one": "1 document coincident",
+ "search.result.other": "# documents coincidents",
+ "search.result.more.one": "1 més en aquesta pàgina",
+ "search.result.more.other": "# més en aquesta pàgina",
+ "search.result.term.missing": "Desaparegut",
+ "select.language": "Selecciona la llengua",
+ "select.version": "Selecciona la versió",
+ "source": "Ves al repositori",
+ "source.file.contributors": "Col·laboradors",
+ "source.file.date.created": "Creada",
+ "source.file.date.updated": "Darrera actualització",
+ "tabs": "Pestanyes",
+ "toc": "Taula de continguts",
+ "top": "Torna a l'inici"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/cs.html b/src/templates/partials/languages/cs.html
new file mode 100644
index 00000000..fb955865
--- /dev/null
+++ b/src/templates/partials/languages/cs.html
@@ -0,0 +1,75 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Czech -->
+{% macro t(key) %}{{ {
+ "language": "cs",
+ "action.edit": "Upravit tuto stránku",
+ "action.skip": "Přeskočit obsah",
+ "action.view": "Zobrazit zdroj této stránky",
+ "announce.dismiss": "Již nezobrazovat",
+ "blog.archive": "Archiv",
+ "blog.categories": "Kategorie",
+ "blog.categories.in": "v",
+ "blog.continue": "Pokračovat ve čtení",
+ "blog.draft": "Návrh",
+ "blog.index": "Zpět na index",
+ "blog.meta": "Metadata",
+ "blog.references": "Související odkazy",
+ "clipboard.copy": "Kopírovat do schránky",
+ "clipboard.copied": "Zkopírováno do schránky",
+ "consent.accept": "Akceptovat",
+ "consent.manage": "Spravovat nastavení",
+ "consent.reject": "Odmítnout",
+ "footer": "Zápatí",
+ "footer.next": "Další",
+ "footer.previous": "Předchozí",
+ "header": "Záhlaví",
+ "meta.comments": "Komentáře",
+ "meta.source": "Zdroj",
+ "nav": "Navigace",
+ "readtime.one": "1 min čtení",
+ "readtime.other": "# min čtení",
+ "rss.created": "RSS kanál",
+ "rss.updated": "RSS zdroj aktualizovaného obsahu",
+ "search": "Vyhledávání",
+ "search.placeholder": "Hledat",
+ "search.share": "Sdílet",
+ "search.reset": "Vyčistit",
+ "search.result.initializer": "Inicializace vyhledávání",
+ "search.result.placeholder": "Pište co se má vyhledat",
+ "search.result.none": "Nenalezeny žádné dokumenty",
+ "search.result.one": "Nalezený dokument: 1",
+ "search.result.other": "Nalezené dokumenty: #",
+ "search.result.more.one": "1 další na této stránce",
+ "search.result.more.other": "# více na této stránce",
+ "search.result.term.missing": "Chybějící",
+ "select.language": "Zvolte jazyk",
+ "select.version": "Vyberte verzi",
+ "source": "Přejít do repozitáře",
+ "source.file.contributors": "Přispěvatelé",
+ "source.file.date.created": "Vytvořeno",
+ "source.file.date.updated": "Poslední aktualizace",
+ "tabs": "Karty",
+ "toc": "Obsah",
+ "top": "Zpět na začátek"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/da.html b/src/templates/partials/languages/da.html
new file mode 100644
index 00000000..2f9da2db
--- /dev/null
+++ b/src/templates/partials/languages/da.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Danish -->
+{% macro t(key) %}{{ {
+ "language": "da",
+ "action.edit": "Redigér denne side",
+ "action.skip": "Gå til indholdet",
+ "action.view": "Vis kildetekst på denne side",
+ "announce.dismiss": "Vis ikke dette igen",
+ "blog.archive": "Arkiv",
+ "blog.categories": "Kategorier",
+ "blog.categories.in": "i",
+ "blog.continue": "Læs mere",
+ "blog.draft": "Udkast",
+ "blog.index": "Gå tilbage",
+ "blog.meta": "Metadata",
+ "blog.references": "Relateret indhold",
+ "clipboard.copy": "Kopiér til udklipsholderen",
+ "clipboard.copied": "Kopieret til udklipsholderen",
+ "consent.accept": "Accepter",
+ "consent.manage": "Administrer indstillinger",
+ "consent.reject": "Afvis",
+ "footer": "Sidefod",
+ "footer.next": "Næste",
+ "footer.previous": "Forrige",
+ "header": "Sidehoved",
+ "meta.comments": "Kommentarer",
+ "meta.source": "Kilde",
+ "nav": "Navigation",
+ "readtime.one": "1 minuts læsetid",
+ "readtime.other": "# minuts læstid",
+ "rss.created": "RSS feed",
+ "rss.updated": "RSS feed af opdateret indhold",
+ "search": "Søg",
+ "search.config.lang": "da",
+ "search.placeholder": "Søg",
+ "search.share": "Del",
+ "search.reset": "Nulstil søgning",
+ "search.result.initializer": "Start søgning",
+ "search.result.placeholder": "Indtast søgeord",
+ "search.result.none": "Ingen resultater fundet",
+ "search.result.one": "1 resultat",
+ "search.result.other": "# resultater",
+ "search.result.more.one": "1 resultat mere på denne side",
+ "search.result.more.other": "# resultater mere på denne side",
+ "search.result.term.missing": "Mangler",
+ "select.language": "Vælg sprog",
+ "select.version": "Vælg version",
+ "source": "Åbn arkiv",
+ "source.file.contributors": "Bidragydere",
+ "source.file.date.created": "Oprettet",
+ "source.file.date.updated": "Sidste ændring",
+ "tabs": "Faner",
+ "toc": "Indholdsfortegnelse",
+ "top": "Tilbage til start"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/de.html b/src/templates/partials/languages/de.html
new file mode 100644
index 00000000..bfd8b909
--- /dev/null
+++ b/src/templates/partials/languages/de.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: German -->
+{% macro t(key) %}{{ {
+ "language": "de",
+ "action.edit": "Seite editieren",
+ "action.skip": "Zum Inhalt",
+ "action.view": "Quellcode der Seite anzeigen",
+ "announce.dismiss": "Nicht mehr anzeigen",
+ "blog.archive": "Archiv",
+ "blog.categories": "Kategorien",
+ "blog.categories.in": "in",
+ "blog.continue": "Weiterlesen",
+ "blog.draft": "Entwurf",
+ "blog.index": "Zur Übersicht",
+ "blog.meta": "Metadaten",
+ "blog.references": "Weiterführende Links",
+ "clipboard.copy": "In Zwischenablage kopieren",
+ "clipboard.copied": "In Zwischenablage kopiert",
+ "consent.accept": "Akzeptieren",
+ "consent.manage": "Einstellungen",
+ "consent.reject": "Ablehnen",
+ "footer": "Fußzeile",
+ "footer.next": "Weiter",
+ "footer.previous": "Zurück",
+ "header": "Kopfzeile",
+ "meta.comments": "Kommentare",
+ "meta.source": "Quellcode",
+ "nav": "Navigation",
+ "readtime.one": "1 Min. Lesezeit",
+ "readtime.other": "# Min. Lesezeit",
+ "rss.created": "RSS Feed",
+ "rss.updated": "RSS Feed der aktualisierten Inhalte",
+ "search": "Suche",
+ "search.config.lang": "de",
+ "search.placeholder": "Suche",
+ "search.share": "Teilen",
+ "search.reset": "Zurücksetzen",
+ "search.result.initializer": "Suche wird initialisiert",
+ "search.result.placeholder": "Suchbegriff eingeben",
+ "search.result.none": "Keine Suchergebnisse",
+ "search.result.one": "1 Suchergebnis",
+ "search.result.other": "# Suchergebnisse",
+ "search.result.more.one": "1 weiteres Suchergebnis auf dieser Seite",
+ "search.result.more.other": "# weitere Suchergebnisse auf dieser Seite",
+ "search.result.term.missing": "Es fehlt",
+ "select.language": "Sprache wechseln",
+ "select.version": "Version auswählen",
+ "source": "Zum Repository",
+ "source.file.contributors": "Mitwirkende",
+ "source.file.date.created": "Erstellt",
+ "source.file.date.updated": "Letztes Update",
+ "tabs": "Hauptnavigation",
+ "toc": "Inhaltsverzeichnis",
+ "top": "Zurück zum Seitenanfang"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/el.html b/src/templates/partials/languages/el.html
new file mode 100644
index 00000000..8dce1793
--- /dev/null
+++ b/src/templates/partials/languages/el.html
@@ -0,0 +1,74 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Greek -->
+{% macro t(key) %}{{ {
+ "language": "el",
+ "action.edit": "Επεξεργασία αυτής της σελίδας",
+ "action.skip": "Μετάβαση στο περιεχόμενο",
+ "action.view": "Προβολή πηγαίου κώδικα",
+ "announce.dismiss": "Μην το ξαναδείξετε αυτό",
+ "blog.archive": "Aρχείο",
+ "blog.categories": "Κατηγορίες",
+ "blog.categories.in": "Στο",
+ "blog.continue": "Περισσότερα",
+ "blog.draft": "Πρόχειρο",
+ "blog.index": "Eπιστροφή",
+ "blog.references": "Σχετικοί σύνδεσμοι",
+ "clipboard.copy": "Αντιγραφή στο πρόχειρο",
+ "clipboard.copied": "Αντιγράφηκε στο πρόχειρο",
+ "consent.accept": "Αποδοχή",
+ "consent.manage": "Περισσότερες επιλογές",
+ "consent.reject": "Απόρριψη",
+ "footer": "Υποσέλιδο",
+ "footer.next": "Επόμενο",
+ "footer.previous": "Προηγούμενο",
+ "header": "Κεφαλίδα",
+ "meta.comments": "Σχόλια",
+ "meta.source": "Πηγή",
+ "nav": "Πλοήγηση",
+ "readtime.one": "1 λεπτό διάβασμα",
+ "readtime.other": "# λεπτά διάβασμα",
+ "rss.created": "Ροές Δεδομένων RSS",
+ "rss.updated": "Ροές Δεδομένων RSS. Τελευταία νέα",
+ "search": "Αναζήτηση",
+ "search.placeholder": "Αναζήτηση",
+ "search.share": "Διαμοίραση",
+ "search.reset": "Καθαρισμός",
+ "search.result.initializer": "Αρχικοποίηση αναζήτησης",
+ "search.result.placeholder": "Πληκτρολογήστε για να αρχίσει η αναζήτηση",
+ "search.result.none": "δεν βρήκε κάποιο έγγραφο",
+ "search.result.one": "1 έγγραφο που ταιριάζει",
+ "search.result.other": "# έγγραφα που ταιριάζουν",
+ "search.result.more.one": "1 ακόμα σε αυτήν τη σελίδα",
+ "search.result.more.other": "# ακόμα σε αυτήν τη σελίδα",
+ "search.result.term.missing": "Λείπει",
+ "select.language": "Επιλογή γλώσσας",
+ "select.version": "Επιλογή έκδοσης",
+ "source": "Μετάβαση στο αποθετήριο",
+ "source.file.contributors": "Συνεισφέροντες",
+ "source.file.date.created": "Δημιουργήθηκε",
+ "source.file.date.updated": "τελευταία ενημέρωση",
+ "tabs": "Καρτέλες",
+ "toc": "Πίνακας περιεχομένων",
+ "top": "Επιστροφή στην αρχή"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/en.html b/src/templates/partials/languages/en.html
new file mode 100644
index 00000000..0e6a73ac
--- /dev/null
+++ b/src/templates/partials/languages/en.html
@@ -0,0 +1,79 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: English -->
+{% macro t(key) %}{{ {
+ "language": "en",
+ "direction": "ltr",
+ "action.edit": "Edit this page",
+ "action.skip": "Skip to content",
+ "action.view": "View source of this page",
+ "announce.dismiss": "Don't show this again",
+ "blog.archive": "Archive",
+ "blog.categories": "Categories",
+ "blog.categories.in": "in",
+ "blog.continue": "Continue reading",
+ "blog.draft": "Draft",
+ "blog.index": "Back to index",
+ "blog.meta": "Metadata",
+ "blog.references": "Related links",
+ "clipboard.copy": "Copy to clipboard",
+ "clipboard.copied": "Copied to clipboard",
+ "consent.accept": "Accept",
+ "consent.manage": "Manage settings",
+ "consent.reject": "Reject",
+ "footer": "Footer",
+ "footer.next": "Next",
+ "footer.previous": "Previous",
+ "header": "Header",
+ "meta.comments": "Comments",
+ "meta.source": "Source",
+ "nav": "Navigation",
+ "readtime.one": "1 min read",
+ "readtime.other": "# min read",
+ "rss.created": "RSS feed",
+ "rss.updated": "RSS feed of updated content",
+ "search": "Search",
+ "search.config.lang": "en",
+ "search.config.pipeline": "stopWordFilter",
+ "search.config.separator": "[\\s\\-]+",
+ "search.placeholder": "Search",
+ "search.share": "Share",
+ "search.reset": "Clear",
+ "search.result.initializer": "Initializing search",
+ "search.result.placeholder": "Type to start searching",
+ "search.result.none": "No matching documents",
+ "search.result.one": "1 matching document",
+ "search.result.other": "# matching documents",
+ "search.result.more.one": "1 more on this page",
+ "search.result.more.other": "# more on this page",
+ "search.result.term.missing": "Missing",
+ "select.language": "Select language",
+ "select.version": "Select version",
+ "source": "Go to repository",
+ "source.file.contributors": "Contributors",
+ "source.file.date.created": "Created",
+ "source.file.date.updated": "Last update",
+ "tabs": "Tabs",
+ "toc": "Table of contents",
+ "top": "Back to top"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/eo.html b/src/templates/partials/languages/eo.html
new file mode 100644
index 00000000..cd3829a8
--- /dev/null
+++ b/src/templates/partials/languages/eo.html
@@ -0,0 +1,49 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Esperanto -->
+{% macro t(key) %}{{ {
+ "language": "eo",
+ "action.edit": "Redakti ĉi tiun paĝon",
+ "action.skip": "Saltu al enhavo",
+ "clipboard.copy": "Kopii al tondujo",
+ "clipboard.copied": "Kopiado al klipo",
+ "footer": "Piedlinio",
+ "footer.next": "Sekva",
+ "footer.previous": "Antaŭa",
+ "header": "Kaplinio",
+ "meta.comments": "Komentoj",
+ "meta.source": "Fontkodo",
+ "nav": "Navigado",
+ "search.config.lang": "es",
+ "search.placeholder": "Serĉo",
+ "search.reset": "Klara",
+ "search.result.placeholder": "Tajpu por komenci serĉadon",
+ "search.result.none": "Neniuj kongruaj dokumentoj",
+ "search.result.one": "1 kongrua dokumento",
+ "search.result.other": "# kongruaj dokumentoj",
+ "source": "Iru al deponejo",
+ "source.file.date.created": "Kreita",
+ "source.file.date.updated": "Lasta ĝisdatigo",
+ "tabs": "Langetoj",
+ "toc": "Enhavtabelo"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/es.html b/src/templates/partials/languages/es.html
new file mode 100644
index 00000000..bbbd9dc1
--- /dev/null
+++ b/src/templates/partials/languages/es.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Spanish -->
+{% macro t(key) %}{{ {
+ "language": "es",
+ "action.edit": "Editar esta página",
+ "action.skip": "Saltar a contenido",
+ "action.view": "Ver código fuente de esta página",
+ "announce.dismiss": "No mostrar esto de nuevo",
+ "blog.archive": "Archivo",
+ "blog.categories": "Categorías",
+ "blog.categories.in": "en",
+ "blog.continue": "Seguir leyendo",
+ "blog.draft": "Borrador",
+ "blog.index": "Regresar al índice",
+ "blog.meta": "Metadata",
+ "blog.references": "Enlaces relacionados",
+ "clipboard.copy": "Copiar al portapapeles",
+ "clipboard.copied": "Copiado al portapapeles",
+ "consent.accept": "Aceptar",
+ "consent.manage": "Gestionar cookies",
+ "consent.reject": "Rechazar",
+ "footer": "Pie",
+ "footer.next": "Siguiente",
+ "footer.previous": "Anterior",
+ "header": "Cabecera",
+ "meta.comments": "Comentarios",
+ "meta.source": "Fuente",
+ "nav": "Navegación",
+ "readtime.one": "1 minuto de lectura",
+ "readtime.other": "# minutos de lectura",
+ "rss.created": "Fuente RSS",
+ "rss.updated": "Fuente RSS de contenido actualizado",
+ "search": "Buscar",
+ "search.config.lang": "es",
+ "search.placeholder": "Búsqueda",
+ "search.share": "Compartir",
+ "search.reset": "Limpiar",
+ "search.result.initializer": "Inicializando búsqueda",
+ "search.result.placeholder": "Teclee para comenzar búsqueda",
+ "search.result.none": "No se encontraron documentos",
+ "search.result.one": "1 documento encontrado",
+ "search.result.other": "# documentos encontrados",
+ "search.result.more.one": "1 más en esta página",
+ "search.result.more.other": "# más en esta página",
+ "search.result.term.missing": "Falta",
+ "select.language": "Seleccionar idioma",
+ "select.version": "Seleccionar versión",
+ "source": "Ir al repositorio",
+ "source.file.contributors": "Contribuidores",
+ "source.file.date.created": "Creado",
+ "source.file.date.updated": "Última actualización",
+ "tabs": "Pestañas",
+ "toc": "Tabla de contenidos",
+ "top": "Volver al principio"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/et.html b/src/templates/partials/languages/et.html
new file mode 100644
index 00000000..8add4225
--- /dev/null
+++ b/src/templates/partials/languages/et.html
@@ -0,0 +1,43 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Estonian -->
+{% macro t(key) %}{{ {
+ "language": "et",
+ "action.edit": "Muuda seda lehte",
+ "action.skip": "Keri sisuni",
+ "clipboard.copy": "Kopeeri lõikelauale",
+ "clipboard.copied": "Kopeeritud",
+ "footer.next": "Järgmine",
+ "footer.previous": "Eelmine",
+ "meta.comments": "Kommentaarid",
+ "meta.source": "Lähtekood",
+ "search.placeholder": "Otsi",
+ "search.result.placeholder": "Otsimiseks kirjuta siia",
+ "search.result.none": "Otsingule ei leitud ühtegi vastet",
+ "search.result.one": "Leiti üks tulemus",
+ "search.result.other": "Leiti # tulemust",
+ "source": "Ava repositooriumis",
+ "source.file.date.created": "Loodud",
+ "source.file.date.updated": "Viimane uuendus",
+ "toc": "Sisukord"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/eu.html b/src/templates/partials/languages/eu.html
new file mode 100644
index 00000000..0e52f925
--- /dev/null
+++ b/src/templates/partials/languages/eu.html
@@ -0,0 +1,75 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Basque -->
+{% macro t(key) %}{{ {
+ "language": "eu",
+ "action.edit": "Editatu orri hau",
+ "action.skip": "Joan zuzenean edukira",
+ "action.view": "Ikusi orri honen iturburua",
+ "announce.dismiss": "Ez erakutsi hau berriro",
+ "blog.archive": "Artxiboa",
+ "blog.categories": "Kategoriak",
+ "blog.categories.in": "kategoria",
+ "blog.continue": "Jarraitu irakurtzen",
+ "blog.draft": "Zirriborroa",
+ "blog.index": "Itzuli aurkibidera",
+ "blog.meta": "Metadatuak",
+ "blog.references": "Erlazionatutako estekak",
+ "clipboard.copy": "Kopiatu arbelean",
+ "clipboard.copied": "Arbelean kopiatuta",
+ "consent.accept": "Onartu",
+ "consent.manage": "Kudeatu ezarpenak",
+ "consent.reject": "Ukatu",
+ "footer": "Orri-oina",
+ "footer.next": "Hurrengoa",
+ "footer.previous": "Aurrekoa",
+ "header": "Atalburua",
+ "meta.comments": "Iruzkinak",
+ "meta.source": "Iturburua",
+ "nav": "Nabigazioa",
+ "readtime.one": "Minutu batean irakurtzeko",
+ "readtime.other": "# minututan irakurtzeko",
+ "rss.created": "RSS jarioa",
+ "rss.updated": "Eduki eguneratuen RSS jarioa",
+ "search": "Bilatu",
+ "search.placeholder": "Bilatu",
+ "search.share": "Partekatu",
+ "search.reset": "Garbitu",
+ "search.result.initializer": "Bilaketa hasieratzen",
+ "search.result.placeholder": "Idatzi bilatzen hasteko",
+ "search.result.none": "Bat datorren dokumenturik ez",
+ "search.result.one": "Bat datorren dokumentu bat",
+ "search.result.other": "Bat datozen # dokumentu",
+ "search.result.more.one": "Bat gehiago orri honetan",
+ "search.result.more.other": "# gehiago orri honetan",
+ "search.result.term.missing": "Falta da",
+ "select.language": "Hautatu hizkuntza",
+ "select.version": "Hautatu bertsioa",
+ "source": "Joan biltegira",
+ "source.file.contributors": "Kolaboratzaileak",
+ "source.file.date.created": "Sortze data",
+ "source.file.date.updated": "Azken eguneratzea",
+ "tabs": "Fitxak",
+ "toc": "Edukiak",
+ "top": "Igo goraino"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/fa.html b/src/templates/partials/languages/fa.html
new file mode 100644
index 00000000..deaa8bca
--- /dev/null
+++ b/src/templates/partials/languages/fa.html
@@ -0,0 +1,77 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Persian (Farsi) -->
+{% macro t(key) %}{{ {
+ "language": "fa",
+ "direction": "rtl",
+ "action.edit": "این صفحه را ویرایش کنید",
+ "action.skip": "پرش به محتویات",
+ "action.view": "محتویات این صفحه را نشان بده",
+ "announce.dismiss": "این را دیگر نشان نده",
+ "blog.archive": "بایگانی",
+ "blog.categories": "دسته‌بندی‌ها",
+ "blog.categories.in": "در",
+ "blog.continue": "ادامه به خواندن",
+ "blog.draft": "پیش‌نویس",
+ "blog.index": "برگشت به فهرست",
+ "blog.meta": "فراداده",
+ "blog.references": "پیوندهای مربوط",
+ "clipboard.copy": "کپی کردن",
+ "clipboard.copied": "کپی شد",
+ "consent.accept": "تایید",
+ "consent.manage": "مدیریت تنظیمات",
+ "consent.reject": "رد کردن",
+ "footer": "پاورقی",
+ "footer.next": "بعدی",
+ "footer.previous": "قبلی",
+ "header": "سرتیتر",
+ "meta.comments": "نظرات",
+ "meta.source": "منبع",
+ "nav": "هدایت",
+ "readtime.one": "1 دقیقه زمان خواندن",
+ "readtime.other": "# دقیقه زمان خواندن",
+ "rss.created": "خوراک آراس‌اس",
+ "rss.updated": "خوراک آراساس محتویات به‌روز شده",
+ "search": "جستجو",
+ "search.config.pipeline": " ",
+ "search.placeholder": "جستجو",
+ "search.share": "هم‌رسانی",
+ "search.reset": "بازنشانی",
+ "search.result.initializer": "راه‌اندازی جستجو",
+ "search.result.placeholder": "برای شروع جستجو تایپ کنید",
+ "search.result.none": "سندی یافت نشد",
+ "search.result.one": "1 سند یافت شد",
+ "search.result.other": "# سند یافت شد",
+ "search.result.more.one": "1 مورد دیگر در این صفحه",
+ "search.result.more.other": "# مورد دیگر در این صفحه",
+ "search.result.term.missing": "موجود نیست",
+ "select.language": "انتخاب زبان",
+ "select.version": "انتخاب ویرایش",
+ "source": "رفتن به مخزن",
+ "source.file.contributors": "مشارکت کنندگان",
+ "source.file.date.created": "ایجاد شده",
+ "source.file.date.updated": "اخرین بروزرسانی",
+ "tabs": "زبانه‌ها",
+ "toc": "فهرست موضوعات",
+ "top": "برگشت به بالا"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/fi.html b/src/templates/partials/languages/fi.html
new file mode 100644
index 00000000..8ee09122
--- /dev/null
+++ b/src/templates/partials/languages/fi.html
@@ -0,0 +1,44 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Finnish -->
+{% macro t(key) %}{{ {
+ "language": "fi",
+ "action.edit": "Muokkaa tätä sivua",
+ "action.skip": "Hyppää sisältöön",
+ "clipboard.copy": "Kopioi leikepöydälle",
+ "clipboard.copied": "Kopioitu leikepöydälle",
+ "footer.next": "Seuraava",
+ "footer.previous": "Edellinen",
+ "meta.comments": "Kommentit",
+ "meta.source": "Lähdekodi",
+ "search.config.lang": "fi",
+ "search.placeholder": "Hae",
+ "search.result.placeholder": "Kirjoita aloittaaksesi haun",
+ "search.result.none": "Ei täsmääviä dokumentteja",
+ "search.result.one": "1 täsmäävä dokumentti",
+ "search.result.other": "# täsmäävää dokumenttia",
+ "source": "Mene repositoryyn",
+ "source.file.date.created": "Luotu",
+ "source.file.date.updated": "Viimeisin päivitys",
+ "toc": "Sisällysluettelo"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/fr.html b/src/templates/partials/languages/fr.html
new file mode 100644
index 00000000..9d49535a
--- /dev/null
+++ b/src/templates/partials/languages/fr.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: French -->
+{% macro t(key) %}{{ {
+ "language": "fr",
+ "action.edit": "Editer cette page",
+ "action.skip": "Aller au contenu",
+ "action.view": "Consulter la source de cette page",
+ "announce.dismiss": "Ne plus montrer cela",
+ "blog.archive": "Archive",
+ "blog.categories": "Catégories",
+ "blog.categories.in": "dans",
+ "blog.continue": "Continuer à lire",
+ "blog.draft": "Brouillon",
+ "blog.index": "Retourner à l'index",
+ "blog.meta": "Metadonnées",
+ "blog.references": "Liens connexes",
+ "clipboard.copy": "Copier dans le presse-papier",
+ "clipboard.copied": "Copié dans le presse-papier",
+ "consent.accept": "Accepter",
+ "consent.manage": "Paramétrer vos choix",
+ "consent.reject": "Refuser",
+ "footer": "Pied de page",
+ "footer.next": "Suivant",
+ "footer.previous": "Précédent",
+ "header": "En-tête",
+ "meta.comments": "Commentaires",
+ "meta.source": "Source",
+ "nav": "Navigation",
+ "readtime.one": "1 min de lecture",
+ "readtime.other": "# min de lecture",
+ "rss.created": "Flux RSS",
+ "rss.updated": "Flux RSS du contenu mis à jour",
+ "search": "Recherche",
+ "search.config.lang": "fr",
+ "search.placeholder": "Rechercher",
+ "search.share": "Partager",
+ "search.reset": "Effacer",
+ "search.result.initializer": "Initialisation de la recherche",
+ "search.result.placeholder": "Taper pour démarrer la recherche",
+ "search.result.none": "Aucun document trouvé",
+ "search.result.one": "1 document trouvé",
+ "search.result.other": "# documents trouvés",
+ "search.result.more.one": "1 de plus sur cette page",
+ "search.result.more.other": "# de plus sur cette page",
+ "search.result.term.missing": "Non trouvé",
+ "select.language": "Sélectionner la langue",
+ "select.version": "Sélectionner la version",
+ "source": "Aller au dépôt",
+ "source.file.contributors": "Contributeurs",
+ "source.file.date.created": "Créé",
+ "source.file.date.updated": "Dernière mise à jour",
+ "tabs": "Onglets",
+ "toc": "Table des matières",
+ "top": "Retour en haut de la page"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/gl.html b/src/templates/partials/languages/gl.html
new file mode 100644
index 00000000..ecb54ffd
--- /dev/null
+++ b/src/templates/partials/languages/gl.html
@@ -0,0 +1,56 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Galician -->
+{% macro t(key) %}{{ {
+ "language": "gl",
+ "action.edit": "Editar esta páxina",
+ "action.skip": "Ir ao contido",
+ "clipboard.copy": "Copiar no cortapapeis",
+ "clipboard.copied": "Copiado no cortapapeis",
+ "footer": "Pé",
+ "footer.next": "Seguinte",
+ "footer.previous": "Anterior",
+ "header": "Cabeceira",
+ "meta.comments": "Comentarios",
+ "meta.source": "Fonte",
+ "nav": "Navegación",
+ "search.config.lang": "es",
+ "search.placeholder": "Procura",
+ "search.reset": "Limpar",
+ "search.result.initializer": "Inicializando procura",
+ "search.result.placeholder": "Insira un termo",
+ "search.result.none": "Sen resultados",
+ "search.result.one": "1 resultado atopado",
+ "search.result.other": "# resultados atopados",
+ "search.result.more.one": "1 máis nesta páxina",
+ "search.result.more.other": "# máis nesta páxina",
+ "search.result.term.missing": "Falta",
+ "select.language": "Seleccionar idioma",
+ "select.version": "Seleccionar version",
+ "source": "Ir ao repositorio",
+ "source.file.date.created": "Creada",
+ "source.file.date.updated": "Última actualización",
+ "tabs": "Pestanas",
+ "toc": "Táboa de contidos",
+ "top": "Volver ao principio"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/he.html b/src/templates/partials/languages/he.html
new file mode 100644
index 00000000..128edab5
--- /dev/null
+++ b/src/templates/partials/languages/he.html
@@ -0,0 +1,77 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Hebrew -->
+{% macro t(key) %}{{ {
+ "language": "he",
+ "direction": "rtl",
+ "action.edit": "עריכת הדף הזה",
+ "action.skip": "לדלג לתוכן",
+ "action.view": "צפה במקור של דף זה",
+ "announce.dismiss": "לא להציג את זה שוב",
+ "blog.archive": "ארכיון",
+ "blog.categories": "קטגוריות",
+ "blog.categories.in": "בתוך",
+ "blog.continue": "המשך לקרוא",
+ "blog.draft": "טיוטה",
+ "blog.index": "חזרה לאינדקס",
+ "blog.meta": "מטא-נתונים",
+ "blog.references": "קישורים קשורים",
+ "clipboard.copy": "העתקה ללוח",
+ "clipboard.copied": "הועתק ללוח",
+ "consent.accept": "לקבל",
+ "consent.manage": "לנהל הגדרות",
+ "consent.reject": "לדחות",
+ "footer": "כותרת תחתונה",
+ "footer.next": "הבא",
+ "footer.previous": "הקודם",
+ "header": "כותרת עליונה",
+ "meta.comments": "הערות",
+ "meta.source": "מקור",
+ "nav": "ניווט",
+ "readtime.one": "קריאה 1 דקות",
+ "readtime.other": "# דקות קריאה",
+ "rss.created": "RSS הזנת",
+ "rss.updated": "הזנת RSS של תוכן מעודכן",
+ "search": "חיפוש",
+ "search.config.pipeline": " ",
+ "search.placeholder": "חיפוש",
+ "search.share": "שיתוף",
+ "search.reset": "ניקוי",
+ "search.result.initializer": "אתחול חיפוש",
+ "search.result.placeholder": "יש להקליד כדי להתחיל לחפש",
+ "search.result.none": "אין מסמכים תואמים",
+ "search.result.one": "מסמך1 תואם",
+ "search.result.other": "# מסמך תואם",
+ "search.result.more.one": "עוד אחד בדף הזה",
+ "search.result.more.other": "עוד # בדף הזה",
+ "search.result.term.missing": "חסר",
+ "select.language": "בחירת שפה",
+ "select.version": "בחירת גרסה",
+ "source": "לעבור אל המאגר",
+ "source.file.contributors": "תורמים",
+ "source.file.date.created": "נוצר",
+ "source.file.date.updated": "עדכון אחרון",
+ "tabs": "לשוניות",
+ "toc": "תוכן העניינים",
+ "top": "חזרה למעלה"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/hi.html b/src/templates/partials/languages/hi.html
new file mode 100644
index 00000000..8c27c259
--- /dev/null
+++ b/src/templates/partials/languages/hi.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Hindi -->
+{% macro t(key) %}{{ {
+ "language": "hi",
+ "action.edit": "इस पृष्ठ को संपादित करें",
+ "action.skip": "विषय पर बढ़ें",
+ "action.view": "इस पृष्ठ का सूत्र देखें",
+ "announce.dismiss": "इसे पुनः न दिखायें",
+ "blog.archive": "पुरालेख",
+ "blog.categories": "वर्ग",
+ "blog.categories.in": "में",
+ "blog.continue": "पढ़ते रहिये",
+ "blog.draft": "मसौदा",
+ "blog.index": "सूचि को लौटें",
+ "blog.meta": "मेटाडेटा",
+ "blog.references": "सम्बंधित लिंक",
+ "clipboard.copy": "क्लिपबोर्ड पर कॉपी करें",
+ "clipboard.copied": "क्लिपबोर्ड पर कॉपी कर दिया गया",
+ "consent.accept": "स्वीकार करें",
+ "consent.manage": "सेटिंग्स मैनेज करें",
+ "consent.reject": "अस्वीकार करें",
+ "footer": "फुटर",
+ "footer.next": "आगामी",
+ "footer.previous": "पिछला",
+ "header": "शीर्षक",
+ "meta.comments": "टिप्पणियाँ",
+ "meta.source": "स्रोत",
+ "nav": "नैविगेशन",
+ "readtime.one": "1 मिनट पढ़ने को",
+ "readtime.other": "# मिनट पढ़ने को",
+ "rss.created": "RSS फीड",
+ "rss.updated": "नवीनतम विषयवस्तु का RSS feed",
+ "search": "खोजें",
+ "search.config.lang": "hi",
+ "search.placeholder": "खोज",
+ "search.share": "शेयर करें",
+ "search.reset": "हटा दें",
+ "search.result.initializer": "खोज शुरू करें",
+ "search.result.placeholder": "खोज शुरू करने के लिए टाइप करें",
+ "search.result.none": "कोई मिलान डॉक्यूमेंट नहीं",
+ "search.result.one": "1 मिलान डॉक्यूमेंट",
+ "search.result.other": "# मिलान डाक्यूमेंट्स",
+ "search.result.more.one": "1 और इस पृष्ठ पर",
+ "search.result.more.other": "# और इस पृष्ठ पर",
+ "search.result.term.missing": "ग़ायब",
+ "select.language": "भाषा चुनें",
+ "select.version": "वर्शन चुनें",
+ "source": "रिपॉजिटरी पर जाएं",
+ "source.file.contributors": "योगदाता",
+ "source.file.date.created": "बनाया था",
+ "source.file.date.updated": "आखिरी अपडेट",
+ "tabs": "टैब",
+ "toc": "विषय - सूची",
+ "top": "शीर्षभाग पर लौटें"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/hr.html b/src/templates/partials/languages/hr.html
new file mode 100644
index 00000000..30afe6a9
--- /dev/null
+++ b/src/templates/partials/languages/hr.html
@@ -0,0 +1,75 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Croatian -->
+{% macro t(key) %}{{ {
+ "language": "hr",
+ "action.edit": "Uredi stranicu",
+ "action.skip": "Preskoči na sadržaj",
+ "action.view": "Pregledaj izvorni kod ove stranice",
+ "announce.dismiss": "Ne prikazuj ovo opet",
+ "blog.archive": "Arhiva",
+ "blog.categories": "Kategorije",
+ "blog.categories.in": "u",
+ "blog.continue": "Nastavi čitati",
+ "blog.draft": "Nacrt",
+ "blog.index": "Natrag na indeks",
+ "blog.meta": "Metapodaci",
+ "blog.references": "Srodne poveznice",
+ "clipboard.copy": "Kopiraj u međuspremnik",
+ "clipboard.copied": "Kopirano u međuspremnik",
+ "consent.accept": "Prihvati",
+ "consent.manage": "Upravljaj postavkama",
+ "consent.reject": "Odbij",
+ "footer": "Podnožje",
+ "footer.next": "Sljedeće",
+ "footer.previous": "Prethodno",
+ "header": "Zaglavlje",
+ "meta.comments": "Komentari",
+ "meta.source": "Izvor",
+ "nav": "Navigacija",
+ "readtime.one": "1 minuta čitanja",
+ "readtime.other": "# minut(a/e) čitanja",
+ "rss.created": "RSS izvor",
+ "rss.updated": "RSS izvor osvježenog sadržaja",
+ "search": "Pretraživanje",
+ "search.placeholder": "Pretraži",
+ "search.share": "Podijeli",
+ "search.reset": "Očisti",
+ "search.result.initializer": "Inicijaliziranje pretraživanja",
+ "search.result.placeholder": "Unesite pojam pretraživanja",
+ "search.result.none": "Ništa nije pronađeno",
+ "search.result.one": "1 rezultat pretraživanja",
+ "search.result.other": "# rezultat(a) pretraživanja",
+ "search.result.more.one": "1 rezultat na ovoj stranici",
+ "search.result.more.other": "# rezultat(a) na ovoj stranici",
+ "search.result.term.missing": "Nedostaje",
+ "select.language": "Odabir jezika",
+ "select.version": "Odabir verzije",
+ "source": "Idi u repozitorij",
+ "source.file.contributors": "Suradnici",
+ "source.file.date.created": "Stvaranje",
+ "source.file.date.updated": "Posljednje ažuriranje",
+ "tabs": "Kartice",
+ "toc": "Sadržaj",
+ "top": "Vrati se na vrh"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/hu.html b/src/templates/partials/languages/hu.html
new file mode 100644
index 00000000..44798dd8
--- /dev/null
+++ b/src/templates/partials/languages/hu.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Hungarian -->
+{% macro t(key) %}{{ {
+ "language": "hu",
+ "action.edit": "Oldal szerkesztése",
+ "action.skip": "Kihagyás",
+ "action.view": "Oldal forrásának megtekintése",
+ "announce.dismiss": "Ne mutasd többet",
+ "blog.archive": "Archívum",
+ "blog.categories": "Kategóriák",
+ "blog.categories.in": "Kategória:",
+ "blog.continue": "Folytatás",
+ "blog.draft": "Piszkozat",
+ "blog.index": "Vissza a főoldalra",
+ "blog.meta": "Metaadat",
+ "blog.references": "Kapcsolódó linkek",
+ "clipboard.copy": "Másolás vágólapra",
+ "clipboard.copied": "Vágólapra másolva",
+ "consent.accept": "Elfogadás",
+ "consent.manage": "Beállítások",
+ "consent.reject": "Visszautasítás",
+ "footer": "Élőláb",
+ "footer.next": "Következő",
+ "footer.previous": "Előző",
+ "header": "Élőfej",
+ "meta.comments": "Hozzászólások",
+ "meta.source": "Forrás",
+ "nav": "Navigáció",
+ "readtime.one": "1 percnyi",
+ "readtime.other": "# percnyi",
+ "rss.created": "RSS feed",
+ "rss.updated": "Frissített tartalom RSS feedje",
+ "search": "Keresés",
+ "search.config.lang": "hu",
+ "search.placeholder": "Keresés",
+ "search.share": "Megosztás",
+ "search.reset": "Törlés",
+ "search.result.initializer": "Keresés inicializálása",
+ "search.result.placeholder": "Kereséshez írj ide valamit",
+ "search.result.none": "Nincs találat",
+ "search.result.one": "1 egyező dokumentum",
+ "search.result.other": "# egyező dokumentum",
+ "search.result.more.one": "1 további találat az oldalon",
+ "search.result.more.other": "# további találat az oldalon",
+ "search.result.term.missing": "Üres",
+ "select.language": "Nyelvváltás",
+ "select.version": "Verzióváltás",
+ "source": "Főoldalra ugrás",
+ "source.file.contributors": "Szerzők",
+ "source.file.date.created": "Létrehozva",
+ "source.file.date.updated": "Utolsó frissítés",
+ "tabs": "Lapok",
+ "toc": "Tartalomjegyzék",
+ "top": "Vissza a tetejére"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/hy.html b/src/templates/partials/languages/hy.html
new file mode 100644
index 00000000..e3418341
--- /dev/null
+++ b/src/templates/partials/languages/hy.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Armenian -->
+{% macro t(key) %}{{ {
+ "language": "hy",
+ "action.edit": "Խմբագրել այս էջը",
+ "action.skip": "Անցնել պարունակությանը",
+ "action.view": "Դիտել այս էջի սկզբնաղբյուրը",
+ "announce.dismiss": "Այլևս չցուցադրել",
+ "blog.archive": "Արխիվ",
+ "blog.categories": "Կատեգորիաներ",
+ "blog.categories.in": "in",
+ "blog.continue": "Շարունակել կարդալ",
+ "blog.draft": "Սևագիր",
+ "blog.index": "Հետ դեպի ինդեքս",
+ "blog.meta": "Մետատվյալներ",
+ "blog.references": "Առնչվող հղումներ",
+ "clipboard.copy": "Պատճենել",
+ "clipboard.copied": "Պատճենված է",
+ "consent.accept": "Ընդունել",
+ "consent.manage": "Կառավարել կարգավորումները",
+ "consent.reject": "Մերժել",
+ "footer": "Էջատակ",
+ "footer.next": "Հաջորդը",
+ "footer.previous": "Նախորդը",
+ "header": "Գլխագիր",
+ "meta.comments": "Մեկնաբանությունները",
+ "meta.source": "Աղբյուր",
+ "nav": "Տեղորոշում",
+ "readtime.one": "Ընթերցում՝ 1 րոպե",
+ "readtime.other": "Ընթերցում՝ # րոպե",
+ "rss.created": "RSS հոսք",
+ "rss.updated": "Արդիացված բովանդակության RSS հոսք",
+ "search": "Որոնում",
+ "search.config.pipeline": " ",
+ "search.placeholder": "Որոնում",
+ "search.share": "Կիսվել",
+ "search.reset": "Ջնջել",
+ "search.result.initializer": "Որոնում",
+ "search.result.placeholder": "Մուտքագրեք որոնելու համար",
+ "search.result.none": "Արդյունքներ չկան",
+ "search.result.one": "1 արդյունք",
+ "search.result.other": "# արդյունք",
+ "search.result.more.one": "ևս 1-ը այս էջում",
+ "search.result.more.other": "ևս #-ը այս էջում",
+ "search.result.term.missing": "Բացակայում է",
+ "select.language": "Ընտրել լեզուն",
+ "select.version": "Ընտրել տարբերակը",
+ "source": "Դեպի պահոց",
+ "source.file.contributors": "Հեղինակողներ",
+ "source.file.date.created": "Ստեղծված է",
+ "source.file.date.updated": "Վերջին թարմացումը",
+ "tabs": "Ներդիրներ",
+ "toc": "Բովանդակություն",
+ "top": "Վերադառնալ սկիզբ"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/id.html b/src/templates/partials/languages/id.html
new file mode 100644
index 00000000..c54229f0
--- /dev/null
+++ b/src/templates/partials/languages/id.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Indonesian -->
+{% macro t(key) %}{{ {
+ "language": "id",
+ "action.edit": "Ubah halaman ini",
+ "action.skip": "Lewati ke isi",
+ "action.view": "Lihat sumber halaman ini",
+ "announce.dismiss": "Jangan lihat ini lagi",
+ "blog.archive": "Arsip",
+ "blog.categories": "Kategori",
+ "blog.categories.in": "dalam",
+ "blog.continue": "Lanjut membaca",
+ "blog.draft": "Draf",
+ "blog.index": "Kembali ke indeks",
+ "blog.meta": "Metadata",
+ "blog.references": "Tautan yang berhubungan",
+ "clipboard.copy": "Salin ke clipboard",
+ "clipboard.copied": "Tersalin ke clipboard",
+ "consent.accept": "Terima",
+ "consent.manage": "Kelola pengaturan",
+ "consent.reject": "Tolak",
+ "footer": "Footer",
+ "footer.next": "Selanjutnya",
+ "footer.previous": "Sebelumnya",
+ "header": "Header",
+ "meta.comments": "Komentar",
+ "meta.source": "Sumber",
+ "nav": "Navigasi",
+ "readtime.one": "1 menit baca",
+ "readtime.other": "# menit baca",
+ "rss.created": "Umpan RSS",
+ "rss.updated": "Umpan RSS dari konten yang diperbarui",
+ "search": "Cari",
+ "search.config.pipeline": " ",
+ "search.placeholder": "Cari",
+ "search.share": "Bagikan",
+ "search.reset": "Kosongkan",
+ "search.result.initializer": "Mempersiapkan pencarian",
+ "search.result.placeholder": "Ketik untuk mulai pencarian",
+ "search.result.none": "Tidak ada dokumen yang sesuai",
+ "search.result.one": "1 dokumen ditemukan",
+ "search.result.other": "# dokumen ditemukan",
+ "search.result.more.one": "1 lagi di halaman ini",
+ "search.result.more.other": "# lagi di halaman ini",
+ "search.result.term.missing": "Tidak ada",
+ "select.language": "Pilih bahasa",
+ "select.version": "Pilih versi",
+ "source": "Ke repositori",
+ "source.file.contributors": "Kontributor",
+ "source.file.date.created": "Dibuat",
+ "source.file.date.updated": "Pembaruan terakhir",
+ "tabs": "Tab",
+ "toc": "Daftar isi",
+ "top": "Kembali ke atas"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/is.html b/src/templates/partials/languages/is.html
new file mode 100644
index 00000000..5b9a47e8
--- /dev/null
+++ b/src/templates/partials/languages/is.html
@@ -0,0 +1,75 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Icelandic -->
+{% macro t(key) %}{{ {
+ "language": "is",
+ "action.edit": "Breyta þessari síðu",
+ "action.skip": "Hoppa yfir í efnið",
+ "action.view": "Skoða frumgögn þessarar síðu",
+ "announce.dismiss": "Ekki sýna þetta aftur",
+ "blog.archive": "Safn",
+ "blog.categories": "Flokkar",
+ "blog.categories.in": "í",
+ "blog.continue": "Lesa meira",
+ "blog.draft": "Uppkast",
+ "blog.index": "Til baka í yfirlit",
+ "blog.meta": "Lýsigögn",
+ "blog.references": "Þessu tengt",
+ "clipboard.copy": "Afrita á klemmuspjald",
+ "clipboard.copied": "Afritað á klemmuspjald",
+ "consent.accept": "Samþykkja",
+ "consent.manage": "Breyta stillingum",
+ "consent.reject": "Hafna",
+ "footer": "Síðufótur",
+ "footer.next": "Næsta",
+ "footer.previous": "Fyrri",
+ "header": "Haus",
+ "meta.comments": "Umræður",
+ "meta.source": "Frumgögn",
+ "nav": "Valmynd",
+ "readtime.one": "1 mín lestur",
+ "readtime.other": "# mín lestur",
+ "rss.created": "RSS veita",
+ "rss.updated": "RSS veita fyrir uppfært innihald",
+ "search": "Leita",
+ "search.placeholder": "Leita",
+ "search.share": "Deila",
+ "search.reset": "Hreinsa",
+ "search.result.initializer": "Ræsi leitarvél",
+ "search.result.placeholder": "Byrjaðu að skrifa til að hefja leit",
+ "search.result.none": "Engar síður fundust",
+ "search.result.one": "1 síða fannst",
+ "search.result.other": "# síður fundust",
+ "search.result.more.one": "1 til viðbótar á þessari síðu",
+ "search.result.more.other": "# til viðbótar á þessari síðu",
+ "search.result.term.missing": "Vantar",
+ "select.language": "Veldu tungumál",
+ "select.version": "Veldu útgáfu",
+ "source": "Fara í gagnageymslu",
+ "source.file.contributors": "Meðhöfundar",
+ "source.file.date.created": "Búið til",
+ "source.file.date.updated": "Síðast uppfært",
+ "tabs": "Flipar",
+ "toc": "Efnisyfirlit",
+ "top": "Fara aftur efst"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/it.html b/src/templates/partials/languages/it.html
new file mode 100644
index 00000000..77956ee7
--- /dev/null
+++ b/src/templates/partials/languages/it.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Italian -->
+{% macro t(key) %}{{ {
+ "language": "it",
+ "action.edit": "Modifica",
+ "action.skip": "Vai al contenuto",
+ "action.view": "Vedi il sorgente di questa pagina",
+ "announce.dismiss": "Non mostrare più",
+ "blog.archive": "Archivio",
+ "blog.categories": "Categorie",
+ "blog.categories.in": "in",
+ "blog.continue": "Continua a leggere",
+ "blog.draft": "Bozza",
+ "blog.index": "Torna all'indice",
+ "blog.meta": "Metadati",
+ "blog.references": "Collegamenti",
+ "clipboard.copy": "Copia",
+ "clipboard.copied": "Copiato",
+ "consent.accept": "Accetta",
+ "consent.manage": "Gestisci le opzioni",
+ "consent.reject": "Rifiuta",
+ "footer": "Piede",
+ "footer.next": "Successivo",
+ "footer.previous": "Precedente",
+ "header": "Intestazione",
+ "meta.comments": "Commenti",
+ "meta.source": "Sorgente",
+ "nav": "Navigazione",
+ "readtime.one": "1 minuto di lettura",
+ "readtime.other": "# minuti di lettura",
+ "rss.created": "Feed RSS",
+ "rss.updated": "Contenuto aggiornato del feed RSS",
+ "search": "Cerca",
+ "search.config.lang": "it",
+ "search.placeholder": "Cerca",
+ "search.share": "Condividi",
+ "search.reset": "Cancella",
+ "search.result.initializer": "Inizializza la ricerca",
+ "search.result.placeholder": "Scrivi per iniziare a cercare",
+ "search.result.none": "Nessun documento trovato",
+ "search.result.one": "1 documento trovato",
+ "search.result.other": "# documenti trovati",
+ "search.result.more.one": "1 altro in questa pagina",
+ "search.result.more.other": "# altri in questa pagina",
+ "search.result.term.missing": "Non presente",
+ "select.language": "Seleziona la lingua",
+ "select.version": "Seleziona la versione",
+ "source": "Apri repository",
+ "source.file.contributors": "Contributori",
+ "source.file.date.created": "Creata",
+ "source.file.date.updated": "Ultimo aggiornamento",
+ "tabs": "Tabs",
+ "toc": "Indice",
+ "top": "Torna su"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ja.html b/src/templates/partials/languages/ja.html
new file mode 100644
index 00000000..b4b4279d
--- /dev/null
+++ b/src/templates/partials/languages/ja.html
@@ -0,0 +1,78 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Japanese -->
+{% macro t(key) %}{{ {
+ "language": "ja",
+ "action.edit": "編集",
+ "action.skip": "コンテンツにスキップ",
+ "action.view": "このページの原文を表示",
+ "announce.dismiss": "非表示にします",
+ "blog.archive": "過去の投稿",
+ "blog.categories": "カテゴリー",
+ "blog.categories.in": "",
+ "blog.continue": "続きを読む",
+ "blog.draft": "下書き",
+ "blog.index": "ブログトップへ戻る",
+ "blog.meta": "メタデータ",
+ "blog.references": "関連リンク",
+ "clipboard.copy": "クリップボードへコピー",
+ "clipboard.copied": "コピーしました",
+ "consent.accept": "同意",
+ "consent.manage": "サイトの設定",
+ "consent.reject": "拒否",
+ "footer": "フッター",
+ "footer.next": "次",
+ "footer.previous": "前",
+ "header": "ヘッダー",
+ "meta.comments": "コメント",
+ "meta.source": "ソース",
+ "nav": "ナビゲーション",
+ "readtime.one": "このページは約1分で読めます",
+ "readtime.other": "このページは約#分で読めます",
+ "rss.created": "新しいページのRSSフィード",
+ "rss.updated": "更新されたページのRSSフィード",
+ "search": "検索",
+ "search.config.lang": "ja",
+ "search.config.pipeline": "stemmer",
+ "search.config.separator": "[\\s\\- 、。,.]+",
+ "search.placeholder": "検索",
+ "search.share": "共有",
+ "search.reset": "クリア",
+ "search.result.initializer": "検索を初期化",
+ "search.result.placeholder": "検索キーワードを入力してください",
+ "search.result.none": "何も見つかりませんでした",
+ "search.result.one": "1件見つかりました",
+ "search.result.other": "#件見つかりました",
+ "search.result.more.one": "このページ内にもう1件見つかりました",
+ "search.result.more.other": "このページ内にあと#件見つかりました",
+ "search.result.term.missing": "検索に含まれない",
+ "select.language": "言語切り替え",
+ "select.version": "バージョン切り替え",
+ "source": "リポジトリへ",
+ "source.file.contributors": "投稿者",
+ "source.file.date.created": "作成日",
+ "source.file.date.updated": "最終更新日",
+ "tabs": "タブ",
+ "toc": "目次",
+ "top": "ページトップへ戻る"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ka.html b/src/templates/partials/languages/ka.html
new file mode 100644
index 00000000..edfd2e02
--- /dev/null
+++ b/src/templates/partials/languages/ka.html
@@ -0,0 +1,49 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Georgian -->
+{% macro t(key) %}{{ {
+ "language": "ka",
+ "action.edit": "გვერდის რედარქირება",
+ "action.skip": "კონტენტზე გადასვლა",
+ "clipboard.copy": "კოპირება",
+ "clipboard.copied": "კოპირებულია",
+ "footer.next": "შემდეგი",
+ "footer.previous": "წინა",
+ "meta.comments": "კომენტარები",
+ "meta.source": "წყარო",
+ "nav": "ნავიგაცია",
+ "search.config.pipeline": " ",
+ "search.placeholder": "ძებნა",
+ "search.reset": "გასუფთავება",
+ "search.result.placeholder": "ჩაწერე ძებნის დასაწყებად",
+ "search.result.none": "დოკუმენტი ვერ მოიძებნა",
+ "search.result.one": "მოიძებნა 1 დოკუმენტი",
+ "search.result.other": "მოიძებნა # დოკუმენტი",
+ "search.result.more.one": "კიდევ 1 ამ გვერდზე",
+ "search.result.more.other": "კიდევ # ამ გვერდზე",
+ "source": "საცავში გადასვლა",
+ "source.file.date.created": "შეიქმნა",
+ "source.file.date.updated": "ბოლო განახლება",
+ "tabs": "ტაბები",
+ "toc": "სარჩევი"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/kn.html b/src/templates/partials/languages/kn.html
new file mode 100644
index 00000000..bd0ff722
--- /dev/null
+++ b/src/templates/partials/languages/kn.html
@@ -0,0 +1,75 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Kannada -->
+{% macro t(key) %}{{ {
+ "language": "kn",
+ "action.edit": "ಈ ಪುಟವನ್ನು ತಿದ್ದುಪಡಿ ಮಾಡಿ",
+ "action.skip": "ವಿಷಯಕ್ಕೆ ತೆರಳಿ",
+ "action.view": "ಈ ಪುಟದ ಮೂಲವನ್ನು ವೀಕ್ಷಿಸಿ",
+ "announce.dismiss": "ಇದನ್ನು ಮತ್ತೊಮ್ಮೆ ತೋರಿಸಬೇಡಿ",
+ "blog.archive": "ಹಳೆಯ ಲೇಖನ",
+ "blog.categories": "ವರ್ಗಗಳು",
+ "blog.categories.in": "ರಲ್ಲಿ",
+ "blog.continue": "ಓದು ಮುಂದುವರೆಸಿ",
+ "blog.draft": "ಆರಂಭಿಕ ಬರವಣಿಗೆ",
+ "blog.index": "ಸೂಚ್ಯಂಕಕ್ಕೆ ಹಿಂತಿರುಗಿ",
+ "blog.meta": "ಮಾಹಿತಿಯ ಬಗ್ಗೆ ಮಾಹಿತಿ",
+ "blog.references": "ಸಂಬಂಧಿತ ಉಲ್ಲೇಖಗಳು",
+ "clipboard.copy": "ಇದನ್ನು ನಕಲಿಸಿ",
+ "clipboard.copied": "ಇದನ್ನು ನಕಲು ಮಾಡಿದೆ",
+ "consent.accept": "ನಾನು ಇದನ್ನು ಒಪ್ಪಿಕೊಳ್ಳುತ್ತೇನೆ",
+ "consent.manage": "ಸಂರಚನೆಯನ್ನು ನಿರ್ವಹಿಸಿ",
+ "consent.reject": "ನಾನು ಇದನ್ನು ತಿರಸ್ಕರಿಸುತ್ತೇನೆ",
+ "footer": "ಅಡಿಟಿಪ್ಪಣಿ",
+ "footer.next": "ಮುಂದಿನ ಸಂಚಿಕೆ",
+ "footer.previous": "ಹಿಂದಿನ ಸಂಚಿಕೆ",
+ "header": "ಮೇಲ್ಟಿಪ್ಪಣಿ",
+ "meta.comments": "ಪ್ರತಿಕ್ರಿಯೆಗಳು",
+ "meta.source": "ಮೂಲ",
+ "nav": "ಸಂಚರಣೆ",
+ "readtime.one": "ಓದಲು ೧ ನಿಮಿಷ ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ",
+ "readtime.other": "ಓದಲು # ನಿಮಿಷಗಳನ್ನು ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ",
+ "rss.created": "ಆರ್ಎಸ್ಎಸ್ ಸೇವೆ",
+ "rss.updated": "ಆರ್ಎಸ್ಎಸ್ ಸೇವೆಯಿಂದ ಇತ್ತೀಚಿನ ನವೀಕರಣ",
+ "search": "ಹುಡುಕಿ",
+ "search.placeholder": "ಹುಡುಕಿ",
+ "search.share": "ಹಂಚಿಕೊಳ್ಳಿ",
+ "search.reset": "ಅಳಿಸು",
+ "search.result.initializer": "ಹುಡುಕಾಟವನ್ನು ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ",
+ "search.result.placeholder": "ಬರೆಯುವ ಮೂಲಕ ಹುಡುಕಲು ಪ್ರಾರಂಭಿಸಿ",
+ "search.result.none": "ಹೊಂದಾಣಿಕೆಯಾಗುವ ದಾಖಲೆಗಳಿಲ್ಲ",
+ "search.result.one": "೧ ಹೊಂದಾಣಿಕೆಯ ದಾಖಲೆಯಿದೆ",
+ "search.result.other": "# ಹೊಂದಾಣಿಕೆಯ ದಾಖಲೆಗಳಿವೆ",
+ "search.result.more.one": "ಈ ಪುಟದಲ್ಲಿ ಇನ್ನೂ ಒಂದು ಕಂಡುಬಂದಿದೆ",
+ "search.result.more.other": "ಈ ಪುಟದಲ್ಲಿ ಇನ್ನೂ # ಇವೆ",
+ "search.result.term.missing": "ಕಾಣೆಯಾಗಿದೆ",
+ "select.language": "ಭಾಷೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ",
+ "select.version": "ಆವೃತ್ತಿಯನ್ನು ಆಯ್ಕೆಮಾಡಿ",
+ "source": "ಭಂಡಾರಕ್ಕೆ ಹೋಗಿ",
+ "source.file.contributors": "ಕೊಡುಗೆದಾರರು",
+ "source.file.date.created": "ರಚಿಸಿದ ದಿನಾಂಕ",
+ "source.file.date.updated": "ಕೊನೆಯ ನವೀಕರಣ ದಿನಾಂಕ",
+ "tabs": "ವಿವಿಧ ಕಿಟಕಿಗಳು",
+ "toc": "ವಿಷಯಗಳ ಪಟ್ಟಿ",
+ "top": "ಮೇಲಕ್ಕೆ ಹಿಂತಿರುಗಿ"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ko.html b/src/templates/partials/languages/ko.html
new file mode 100644
index 00000000..adadccb7
--- /dev/null
+++ b/src/templates/partials/languages/ko.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Korean -->
+{% macro t(key) %}{{ {
+ "language": "ko",
+ "action.edit": "이 페이지를 편집",
+ "action.skip": "콘텐츠로 이동",
+ "action.view": "페이지소스 보기",
+ "announce.dismiss": "다시 안보기",
+ "blog.archive": "아카이브",
+ "blog.categories": "카테고리",
+ "blog.categories.in": "카테고리",
+ "blog.continue": "계속 읽기",
+ "blog.draft": "임시 저장",
+ "blog.index": "Index로 돌아가기",
+ "blog.meta": "메타데이터",
+ "blog.references": "관련 링크",
+ "clipboard.copy": "클립보드로 복사",
+ "clipboard.copied": "클립보드에 복사됨",
+ "consent.accept": "동의 허락",
+ "consent.manage": "동의 허락 관리",
+ "consent.reject": "동의 거부",
+ "footer": "하단/푸터",
+ "footer.next": "다음",
+ "footer.previous": "이전",
+ "header": "상단/헤더",
+ "meta.comments": "댓글",
+ "meta.source": "출처",
+ "nav": "네비게이션",
+ "readtime.one": "읽는시간 1분",
+ "readtime.other": "읽는시간 #분",
+ "rss.created": "RSS 피드 생성완료",
+ "rss.updated": "RSS 피드 업데이트완료",
+ "search": "검색",
+ "search.config.pipeline": " ",
+ "search.placeholder": "검색",
+ "search.share": "공유",
+ "search.reset": "지우기",
+ "search.result.initializer": "검색 초기화",
+ "search.result.placeholder": "검색어를 입력하세요",
+ "search.result.none": "검색어와 일치하는 문서가 없습니다",
+ "search.result.one": "1개의 일치하는 문서",
+ "search.result.other": "#개의 일치하는 문서",
+ "search.result.more.one": "이 문서에서 1개의 검색 결과 더 보기",
+ "search.result.more.other": "이 문서에서 #개의 검색 결과 더 보기",
+ "search.result.term.missing": "포함되지 않은 검색어",
+ "select.language": "언어설정",
+ "select.version": "버전 선택",
+ "source": "저장소로 이동",
+ "source.file.contributors": "참여자들",
+ "source.file.date.created": "작성일",
+ "source.file.date.updated": "마지막 업데이트",
+ "tabs": "탭",
+ "toc": "목차",
+ "top": "맨위로"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ku-IQ.html b/src/templates/partials/languages/ku-IQ.html
new file mode 100644
index 00000000..fe9dd1e7
--- /dev/null
+++ b/src/templates/partials/languages/ku-IQ.html
@@ -0,0 +1,64 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Kurdish (Soranî) -->
+{% macro t(key) %}{{ {
+ "language": "ku",
+ "direction": "rtl",
+ "action.edit": "دەستکاری ئەم پەڕە بکە",
+ "action.skip": "ئەم ناوەڕۆکە بپەڕێنە",
+ "action.view": "سەرچاوەی ئەم لاپەڕەیە نیشان بدە",
+ "announce.dismiss": "دووبارە ئەمە پیشان مەدە",
+ "clipboard.copy": "لەبەرگتنەوە بۆ کلیپبۆرد",
+ "clipboard.copied": "لەبەرگیرایەوە بۆ کلیپ بۆرد",
+ "consent.accept": "ڕازیبوون",
+ "consent.manage": "بەڕیوەبردنی ڕیکخستنەکان",
+ "consent.reject": "ڕەتکردنەوە",
+ "footer": "ژێرپەڕە",
+ "footer.next": "دواتر",
+ "footer.previous": "پێشتر",
+ "header": "ناونیشانی بەڕه",
+ "meta.comments": "لێدوانەکان",
+ "meta.source": "سەرجاوە",
+ "nav": "ڕێنیشاندەر",
+ "search": "گەڕان",
+ "search.config.pipeline": " ",
+ "search.placeholder": "گەڕان",
+ "search.share": "گەڕان",
+ "search.reset": "سڕینەوە",
+ "search.result.initializer": "ئامادەکردنی گەڕان",
+ "search.result.placeholder": "بنووسە بۆ دەستپێکردن بە گەڕان",
+ "search.result.none": "هیچ بەڵگەنامەیەکی هاوتا نیە",
+ "search.result.one": "١ بەڵگەنامەی هاوتا",
+ "search.result.other": "بەڵگەنامەی هاوتا #",
+ "search.result.more.one": "١ دانەی تر لەسەر ئەم پەڕەیە",
+ "search.result.more.other": "دانەی تر لەسەر ئەم پەڕەیە #",
+ "search.result.term.missing": "ونبوو",
+ "select.language": "زمان دیاریبکە",
+ "select.version": "وەشان دیاریبکە",
+ "source": "بڕۆ بۆ کۆگا",
+ "source.file.date.created": "دروسکت کرا",
+ "source.file.date.updated": "دوایین نوێکردنەوە",
+ "tabs": "تابەکان",
+ "toc": "خشتەی ناوەڕۆکەکان",
+ "top": "گەڕانەوە بۆ سەرەوە"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/lb.html b/src/templates/partials/languages/lb.html
new file mode 100644
index 00000000..f38c6568
--- /dev/null
+++ b/src/templates/partials/languages/lb.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Luxembourgish -->
+{% macro t(key) %}{{ {
+ "language": "lb",
+ "direction": "ltr",
+ "action.edit": "D'Säit beaarbechten",
+ "action.skip": "Zum Inhalt iwwersprangen",
+ "action.view": "Quellcode uweisen",
+ "announce.dismiss": "Net erëm uweisen",
+ "blog.archive": "Archiv",
+ "blog.categories": "Kategorien",
+ "blog.categories.in": "an",
+ "blog.continue": "Weider liesen",
+ "blog.draft": "Skizz",
+ "blog.index": "Zeréck zum Index",
+ "blog.meta": "Metadaten",
+ "blog.references": "Änlech Links",
+ "clipboard.copy": "Kopéieren",
+ "clipboard.copied": "Kopéiert",
+ "consent.accept": "Accept",
+ "consent.manage": "Astellungen beaarbechten",
+ "consent.reject": "Ofleenen",
+ "footer": "Footer",
+ "footer.next": "Weider",
+ "footer.previous": "Zeréck",
+ "header": "Header",
+ "meta.comments": "Kommentaren",
+ "meta.source": "Quell",
+ "nav": "Navigatioun",
+ "readtime.one": "1 min Liesedauer",
+ "readtime.other": "# min Liesedauer",
+ "rss.created": "RSS feed",
+ "rss.updated": "RSS feed vun aktualiséiertem Inhalt",
+ "search": "Sichen",
+ "search.placeholder": "Sichen",
+ "search.share": "Deelen",
+ "search.reset": "Läschen",
+ "search.result.initializer": "D'Sich gëtt initialiséiert",
+ "search.result.placeholder": "Schreif fir eppes ze sichen",
+ "search.result.none": "Keng zoutreffend Dokumenter",
+ "search.result.one": "1 zoutreffend Dokument",
+ "search.result.other": "# zoutreffend Dokumenter",
+ "search.result.more.one": "1 méi op dëser Säit",
+ "search.result.more.other": "# méi op dëser Säit",
+ "search.result.term.missing": "Feelend",
+ "select.language": "Sprooch auswielen",
+ "select.version": "Versioun auswielen",
+ "source": "Op den Repository goen",
+ "source.file.contributors": "Matwirkender",
+ "source.file.date.created": "Erstallt",
+ "source.file.date.updated": "Läscht update",
+ "tabs": "Tabs",
+ "toc": "Inhaltsverzeichnis",
+ "top": "Zeréck zum Ufank"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/lt.html b/src/templates/partials/languages/lt.html
new file mode 100644
index 00000000..129505f5
--- /dev/null
+++ b/src/templates/partials/languages/lt.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Lithuanian -->
+{% macro t(key) %}{{ {
+ "language": "lt",
+ "action.edit": "Redaguoti šį puslapį",
+ "action.skip": "Pereiti prie turinio",
+ "action.view": "Žiūrėti puslapio šaltinius",
+ "announce.dismiss": "Daugiau neberodyti",
+ "blog.archive": "Archyvas",
+ "blog.categories": "Kategorijos",
+ "blog.categories.in": "į",
+ "blog.continue": "Skaityti toliau",
+ "blog.draft": "Ruošinys",
+ "blog.index": "Grįžti į indeksą",
+ "blog.meta": "Meta duomenys",
+ "blog.references": "Susieja saitai",
+ "clipboard.copy": "Kopijuoti į iškarpinę",
+ "clipboard.copied": "Nukopijuota į iškarpinę",
+ "consent.accept": "Sutikti",
+ "consent.manage": "Redaguoti nustatymus",
+ "consent.reject": "Atmesti",
+ "footer": "Poraštė",
+ "footer.next": "Sekantis",
+ "footer.previous": "Ankstesnis",
+ "header": "Antraštė",
+ "meta.comments": "Komentarai",
+ "meta.source": "Išeitinis kodas",
+ "nav": "Navigacija",
+ "readtime.one": "1 min skaitymo",
+ "readtime.other": "# min skaitymo",
+ "rss.created": "RSS šaltinis",
+ "rss.updated": "RSS šaltinis atnaujinimams",
+ "search": "Paieška",
+ "search.config.pipeline": " ",
+ "search.placeholder": "Paieška",
+ "search.share": "Dalintis",
+ "search.reset": "Išvalyti",
+ "search.result.initializer": "Paieškos inicijavimas",
+ "search.result.placeholder": "Įveskite norėdami pradėti paiešką",
+ "search.result.none": "Atitinkančių dokumentų nerasta",
+ "search.result.one": "1 atitinkantis dokumentas",
+ "search.result.other": "# atitinkantys dokumentai",
+ "search.result.more.one": "Dar 1 šiame puslapyje",
+ "search.result.more.other": "Dar # šiame puslapyje",
+ "search.result.term.missing": "Nerasta",
+ "select.language": "Pasirinkti kalbą",
+ "select.version": "Pasrinkti versiją",
+ "source": "Eiti į saugyklą",
+ "source.file.contributors": "Dalininkai",
+ "source.file.date.created": "Sukurta",
+ "source.file.date.updated": "Paskutinis atnaujinimas",
+ "tabs": "Skirtukai",
+ "toc": "Turinys",
+ "top": "Grįžti į viršų"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/lv.html b/src/templates/partials/languages/lv.html
new file mode 100644
index 00000000..7bcd9ace
--- /dev/null
+++ b/src/templates/partials/languages/lv.html
@@ -0,0 +1,55 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Latvian -->
+{% macro t(key) %}{{ {
+ "language": "lv",
+ "action.edit": "Rediģēt šo lapu",
+ "action.skip": "Pāriet uz saturu",
+ "clipboard.copy": "Kopēt starpliktuvē",
+ "clipboard.copied": "Kopēts starpliktuvē",
+ "footer": "Kājene",
+ "footer.next": "Nākamais",
+ "footer.previous": "Iepriekšējais",
+ "header": "Galvene",
+ "meta.comments": "Komentārs",
+ "meta.source": "Avots",
+ "nav": "Navigācija",
+ "search.placeholder": "Meklēt",
+ "search.reset": "Notīrīt",
+ "search.result.initializer": "Notiek meklēšanas inicializācija",
+ "search.result.placeholder": "Ierakstiet, lai sāktu meklēšanu",
+ "search.result.none": "Nav atbilstošu dokumentu",
+ "search.result.one": "1 atbilstošs dokuments",
+ "search.result.other": "# atbilstoši dokumenti ",
+ "search.result.more.one": "1 šajā lapā",
+ "search.result.more.other": "# un vairāk šajā lapā",
+ "search.result.term.missing": "Trūkstošs",
+ "select.language": "Izvēlies valodu",
+ "select.version": "Izvēlies versiju",
+ "source": "Doties uz repozitoriju",
+ "source.file.date.created": "Izveidots",
+ "source.file.date.updated": "Pēdējoreiz atjaunots",
+ "tabs": "Cilnes",
+ "toc": "Satura rādītājs",
+ "top": "Atpakaļ uz augšu"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/mk.html b/src/templates/partials/languages/mk.html
new file mode 100644
index 00000000..e3dc114c
--- /dev/null
+++ b/src/templates/partials/languages/mk.html
@@ -0,0 +1,56 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Macedonian -->
+{% macro t(key) %}{{ {
+ "language": "mk",
+ "action.edit": "Уредете ја оваа страница",
+ "action.skip": "Прескокнете до содржината",
+ "clipboard.copy": "Копирај во таблата",
+ "clipboard.copied": "Копирано",
+ "footer": "Подножје",
+ "footer.next": "Следно",
+ "footer.previous": "Претходно",
+ "header": "Заглавје",
+ "meta.comments": "Коментари",
+ "meta.source": "Извор",
+ "nav": "Наслов за навигација",
+ "search.config.lang": "ru",
+ "search.placeholder": "Пребарување",
+ "search.reset": "Чисти",
+ "search.result.initializer": "Иницијализирање на пребарувањето",
+ "search.result.placeholder": "Напишете за да започнете со пребарување",
+ "search.result.none": "Нема соодветни документи",
+ "search.result.one": "1 документ што се совпаѓа",
+ "search.result.other": "# соодветни документи",
+ "search.result.more.one": "Уште 1 на оваа страница",
+ "search.result.more.other": "Уште # на оваа страница",
+ "search.result.term.missing": "Недостасува",
+ "select.language": "Изберете јазик",
+ "select.version": "Изберете верзија",
+ "source": "Одете до складиштето",
+ "source.file.date.created": "Создаден",
+ "source.file.date.updated": "Последно ажурирање",
+ "tabs": "Јазичиња",
+ "toc": "Содржина",
+ "top": "Вратете се на почетокот"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/mn.html b/src/templates/partials/languages/mn.html
new file mode 100644
index 00000000..de9002ab
--- /dev/null
+++ b/src/templates/partials/languages/mn.html
@@ -0,0 +1,51 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Mongolian -->
+{% macro t(key) %}{{ {
+ "language": "mn",
+ "action.edit": "Хуудас засварлах",
+ "action.skip": "Агуулгыг алгасах",
+ "clipboard.copy": "Хуулах",
+ "clipboard.copied": "Санах ойд хуулах",
+ "footer": "Хөл",
+ "footer.next": "Дараах",
+ "footer.previous": "Өмнөх",
+ "header": "Толгой",
+ "meta.comments": "Сэтгэгдэл",
+ "meta.source": "Эх үүсвэр",
+ "nav": "Чиглүүлэгч",
+ "search.config.lang": "ru",
+ "search.placeholder": "Хайлт",
+ "search.reset": "Цэвэрлэх",
+ "search.result.placeholder": "Хайлтын үгээ бичнэ үү",
+ "search.result.none": "Таарц илэрсэнгүй",
+ "search.result.one": "1 таарц илэрлээ",
+ "search.result.other": "# Тохирох баримт бичиг",
+ "search.result.more.one": "1 илүү хуудас байна",
+ "search.result.more.other": "# илүү хуудас байна",
+ "source": "Хадгалах сан руу очих",
+ "source.file.date.created": "Үүсгэсэн",
+ "source.file.date.updated": "Сүүлийн шинэчлэлт",
+ "tabs": "Табууд",
+ "toc": "Агуулга"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ms.html b/src/templates/partials/languages/ms.html
new file mode 100644
index 00000000..57b70fc7
--- /dev/null
+++ b/src/templates/partials/languages/ms.html
@@ -0,0 +1,55 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Bahasa Malaysia -->
+{% macro t(key) %}{{ {
+ "language": "ms",
+ "action.edit": "Edit halaman ini",
+ "action.skip": "Langkau tajuk talian",
+ "clipboard.copy": "Salin ke papan keratan",
+ "clipboard.copied": "Disalin ke papan keratan",
+ "footer": "Pengaki",
+ "footer.next" : "Seterusnya",
+ "footer.previous": "Sebelumnya",
+ "header": "Pengepala",
+ "meta.comments": "Komen",
+ "meta.source": "Sumber",
+ "nav": "Navigasi",
+ "search.placeholder": "Cari",
+ "search.reset": "Padam",
+ "search.result.initializer": "Siap carian",
+ "search.result.placeholder": "Taip untuk mula mencari",
+ "search.result.none": "Tiada dokumen yang sepadan",
+ "search.result.one": "1 dokumen yang sepadan",
+ "search.result.other": "# dokumen yang sepadan",
+ "search.result.more.one": "1 lagi di halaman ini",
+ "search.result.more.other": "# lagi di halaman ini",
+ "search.result.term.missing": "Hilang",
+ "select.language": "Pilih bahasa",
+ "select.version": "Pilih versi",
+ "source": "tajuk talian asal",
+ "source.file.date.created": "tarikh fil asal dicipta",
+ "source.file.date.updated": "Tarikh fil dikemas kini",
+ "tabs": "Tab",
+ "toc": "Jadual kandungan",
+ "top": "Kembali ke atas"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/my.html b/src/templates/partials/languages/my.html
new file mode 100644
index 00000000..27ca3ad9
--- /dev/null
+++ b/src/templates/partials/languages/my.html
@@ -0,0 +1,49 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Burmese -->
+{% macro t(key) %}{{ {
+ "language": "my",
+ "action.edit": "ဤ စာမျက်နှာကို ပြင်ရန်",
+ "action.skip": "မာတိကာ သို့ သွားရန်",
+ "clipboard.copy": "ကလစ်ဘုတ် သို့ ကူးယူရန်",
+ "clipboard.copied": "ကလစ်ဘုတ် သို့ ကူယူပြီး",
+ "footer": "အောက်ခြေ",
+ "footer.next": "ရှေ့သို့",
+ "footer.previous": "နောက်သို့",
+ "header": "ခေါင်းပိုင်း",
+ "meta.comments": "မှတ်ချက်များ",
+ "meta.source": "ရင်းမြစ်",
+ "nav": "လမ်းညွှန်",
+ "search.config.pipeline": " ",
+ "search.placeholder": "ရှာရန်",
+ "search.reset": "ရှင်းလင်း",
+ "search.result.placeholder": "ရှာဖွေခြင်းစရန် စာရိုက်ပါ",
+ "search.result.none": "တူညီသော စာရွက်စာတမ်းများ မရှိပါ",
+ "search.result.one": "စာရွက်စာတမ်း ၁ ခု တူညီသည်",
+ "search.result.other": "စာရွက်စာတမ်း # ခု တူညီသည်",
+ "source": "repository သို့ သွားရန်",
+ "source.file.date.created": "နေပြည်တော်",
+ "source.file.date.updated": "နောက်ဆုံး ထုတ်ပြန်ချက်",
+ "tabs": "တက်များ",
+ "toc": "ပါဝင်အကြောင်းအရာများ"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/nb.html b/src/templates/partials/languages/nb.html
new file mode 100644
index 00000000..6be63531
--- /dev/null
+++ b/src/templates/partials/languages/nb.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Norwegian Bokmål -->
+{% macro t(key) %}{{ {
+ "language": "nb",
+ "action.edit": "Rediger denne siden",
+ "action.skip": "Gå til innhold",
+ "action.view": "Vis kildekoden til denne siden",
+ "announce.dismiss": "Ikke vis dette igjen",
+ "blog.archive": "Arkiv",
+ "blog.categories": "Kategorier",
+ "blog.categories.in": "i",
+ "blog.continue": "Fortsett å lese",
+ "blog.draft": "Kladd",
+ "blog.index": "Tilbake til oversikt",
+ "blog.meta": "Metadata",
+ "blog.references": "Relaterte lenker",
+ "clipboard.copy": "Kopier til utklippstavlen",
+ "clipboard.copied": "Kopiert til utklippstavlen",
+ "consent.accept": "Akseptert",
+ "consent.manage": "Innstillinger",
+ "consent.reject": "Reject",
+ "footer": "Footer",
+ "footer.next": "Neste",
+ "footer.previous": "Forrige",
+ "header": "Header",
+ "meta.comments": "Kommentarer",
+ "meta.source": "Kilde",
+ "nav": "Navigasjon",
+ "readtime.one": "lesteid: 1 min",
+ "readtime.other": "lesetid: # min",
+ "rss.created": "RSS feed",
+ "rss.updated": "Oppdatert RSS feed",
+ "search": "Søk",
+ "search.config.lang": "no",
+ "search.placeholder": "Søk",
+ "search.share": "Del",
+ "search.reset": "Nullstill",
+ "search.result.initializer": "Starter søk",
+ "search.result.placeholder": "Skriv søkeord",
+ "search.result.none": "Ingen treff",
+ "search.result.one": "1 treff",
+ "search.result.other": "# treff",
+ "search.result.more.one": "1 til på denne siden",
+ "search.result.more.other": "# flere på denne siden",
+ "search.result.term.missing": "Mangler",
+ "select.language": "Velg språk",
+ "select.version": "Velg versjon",
+ "source": "Gå til kilde",
+ "source.file.contributors": "Bidragsytere",
+ "source.file.date.created": "Opprettet",
+ "source.file.date.updated": "Sist oppdatert",
+ "tabs": "Faner",
+ "toc": "Innholdsliste",
+ "top": "Tilbake til toppen"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/nl.html b/src/templates/partials/languages/nl.html
new file mode 100644
index 00000000..0000fe60
--- /dev/null
+++ b/src/templates/partials/languages/nl.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Dutch -->
+{% macro t(key) %}{{ {
+ "language": "nl",
+ "action.edit": "Wijzig deze pagina",
+ "action.skip": "Ga naar inhoud",
+ "action.view": "Bron van deze pagina bekijken",
+ "announce.dismiss": "Niet meer laten zien",
+ "blog.archive": "Archief",
+ "blog.categories": "Categorieën",
+ "blog.categories.in": "in",
+ "blog.continue": "Doorgaan met lezen",
+ "blog.draft": "Concept",
+ "blog.index": "Terug naar de inhoudsopgave",
+ "blog.meta": "Metadata",
+ "blog.references": "Gerelateerde links",
+ "clipboard.copy": "Kopiëren naar klembord",
+ "clipboard.copied": "Gekopieerd naar klembord",
+ "consent.accept": "Accepteren",
+ "consent.manage": "Instellingen",
+ "consent.reject": "Afwijzen",
+ "footer": "Footer",
+ "footer.next": "Volgende",
+ "footer.previous": "Vorige",
+ "header": "Header",
+ "meta.comments": "Reacties",
+ "meta.source": "Bron",
+ "nav": "Navigatie",
+ "readtime.one": "1 min leestijd",
+ "readtime.other": "# min leestijd",
+ "rss.created": "RSS feed",
+ "rss.updated": "RSS feed met geüpdatet inhoud",
+ "search": "Zoeken",
+ "search.config.lang": "nl",
+ "search.placeholder": "Zoeken",
+ "search.share": "Delen",
+ "search.reset": "Leegmaken",
+ "search.result.initializer": "Zoeken initialiseren",
+ "search.result.placeholder": "Typ om te beginnen met zoeken",
+ "search.result.none": "Geen overeenkomende documenten",
+ "search.result.one": "1 overeenkomende document",
+ "search.result.other": "# overeenkomende documenten",
+ "search.result.more.one": "1 extra overeenkomst op deze pagina",
+ "search.result.more.other": "# extra overeenkomsten op deze pagina",
+ "search.result.term.missing": "Ontbreekt",
+ "select.language": "Selecteer taal",
+ "select.version": "Selecteer versie",
+ "source": "Ga naar repository",
+ "source.file.contributors": "Bijdragers",
+ "source.file.date.created": "Gecreëerd",
+ "source.file.date.updated": "Laatst geüpdatet",
+ "tabs": "Tabs",
+ "toc": "Inhoudsopgave",
+ "top": "Terug naar boven"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/nn.html b/src/templates/partials/languages/nn.html
new file mode 100644
index 00000000..9478bdbe
--- /dev/null
+++ b/src/templates/partials/languages/nn.html
@@ -0,0 +1,62 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Norwegian Nynorsk -->
+{% macro t(key) %}{{ {
+ "language": "nn",
+ "action.edit": "Rediger denne sida",
+ "action.skip": "Gå til innhald",
+ "announce.dismiss": "Ikkje vis dette att",
+ "clipboard.copy": "Kopier til utklippstavla",
+ "clipboard.copied": "Kopiert til utklippstavla",
+ "consent.accept": "Akseptert",
+ "consent.manage": "Innstillinger",
+ "consent.reject": "Reject",
+ "footer": "Footer",
+ "footer.next": "Neste",
+ "footer.previous": "Førre",
+ "header": "Header",
+ "meta.comments": "Kommentarar",
+ "meta.source": "Kjelde",
+ "nav": "Navigasjon",
+ "search": "Søk",
+ "search.config.lang": "no",
+ "search.placeholder": "Søk",
+ "search.share": "Del",
+ "search.reset": "Nullstill",
+ "search.result.initializer": "Startar søk",
+ "search.result.placeholder": "Skriv søkeord",
+ "search.result.none": "Ingen treff",
+ "search.result.one": "1 treff",
+ "search.result.other": "# treff",
+ "search.result.more.one": "1 til på denne sida",
+ "search.result.more.other": "# fleire på denne sida",
+ "search.result.term.missing": "Manglar",
+ "select.language": "Vel språk",
+ "select.version": "Vel versjon",
+ "source": "Gå til kjelde",
+ "source.file.date.created": "Oppretta",
+ "source.file.date.updated": "Sist oppdatert",
+ "tabs": "Faner",
+ "toc": "Innhaldsliste",
+ "top": "Tilbake til toppen"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/pl.html b/src/templates/partials/languages/pl.html
new file mode 100644
index 00000000..7817633a
--- /dev/null
+++ b/src/templates/partials/languages/pl.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Polish -->
+{% macro t(key) %}{{ {
+ "language": "pl",
+ "action.edit": "Edytuj tę stronę",
+ "action.skip": "Przejdź do treści",
+ "action.view": "Zobacz kod źródłowy tej strony",
+ "announce.dismiss": "Nie pokazuj tego ponownie",
+ "blog.archive": "Archiwum",
+ "blog.categories": "Kategorie",
+ "blog.categories.in": "",
+ "blog.continue": "Czytaj dalej",
+ "blog.draft": "Wersja robocza",
+ "blog.index": "Powrót do indeksu",
+ "blog.meta": "Metadane",
+ "blog.references": "Powiązane łącza",
+ "clipboard.copy": "Kopiuj do schowka",
+ "clipboard.copied": "Skopiowano do schowka",
+ "consent.accept": "Akceptuj",
+ "consent.manage": "Zarządzaj ustawieniami",
+ "consent.reject": "Odrzuć",
+ "footer": "Stopka",
+ "footer.next": "Następna strona",
+ "footer.previous": "Poprzednia strona",
+ "header": "Nagłówek",
+ "meta.comments": "Komentarze",
+ "meta.source": "Kod źródłowy",
+ "nav": "Nawigacja",
+ "readtime.one": "Czas czytania: 1 min",
+ "readtime.other": "Czas czytania: # min",
+ "rss.created": "Kanał RSS",
+ "rss.updated": "Kanał RSS zaktualizowanych treści",
+ "search": "Szukaj",
+ "search.config.pipeline": " ",
+ "search.placeholder": "Szukaj",
+ "search.share": "Udostępnij",
+ "search.reset": "Wyczyść",
+ "search.result.initializer": "Inicjowanie wyszukiwania",
+ "search.result.placeholder": "Zacznij pisać, aby szukać",
+ "search.result.none": "Brak wyników wyszukiwania",
+ "search.result.one": "Wyniki wyszukiwania: 1",
+ "search.result.other": "Wyniki wyszukiwania: #",
+ "search.result.more.one": "1 więcej na tej stronie",
+ "search.result.more.other": "# więcej na tej stronie",
+ "search.result.term.missing": "Brak",
+ "select.language": "Wybierz język",
+ "select.version": "Wybierz wersję",
+ "source": "Przejdź do repozytorium",
+ "source.file.contributors": "Kontrybutorzy",
+ "source.file.date.created": "Utworzony",
+ "source.file.date.updated": "Ostatnia aktualizacja",
+ "tabs": "Zakładki",
+ "toc": "Spis treści",
+ "top": "Powrót do góry"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/pt-BR.html b/src/templates/partials/languages/pt-BR.html
new file mode 100644
index 00000000..d934a9ac
--- /dev/null
+++ b/src/templates/partials/languages/pt-BR.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Portuguese (Brasilian) -->
+{% macro t(key) %}{{ {
+ "language": "pt",
+ "action.edit": "Editar esta página",
+ "action.skip": "Pular para conteúdo",
+ "action.view": "Exibir fonte desta página",
+ "announce.dismiss": "Não mostrar isso novamente",
+ "blog.archive": "Arquivo",
+ "blog.categories": "Categorias",
+ "blog.categories.in": "em",
+ "blog.continue": "Continuar leitura",
+ "blog.draft": "Rascunho",
+ "blog.index": "Voltar ao índice",
+ "blog.meta": "Metadados",
+ "blog.references": "Links relacionados",
+ "clipboard.copy": "Copiar para área de transferência",
+ "clipboard.copied": "Copiado para área de transferência",
+ "consent.accept": "Aceitar",
+ "consent.manage": "Gerenciar configurações",
+ "consent.reject": "Rejeitar",
+ "footer": "Rodapé",
+ "footer.next": "Próximo",
+ "footer.previous": "Anterior",
+ "header": "Cabeçalho",
+ "meta.comments": "Comentários",
+ "meta.source": "Origem",
+ "nav": "Navegação",
+ "readtime.one": "1 min de leitura",
+ "readtime.other": "# min de leitura",
+ "rss.created": "RSS feed",
+ "rss.updated": "RSS feed de conteúdo atualizado",
+ "search": "Pesquisar",
+ "search.config.lang": "pt",
+ "search.placeholder": "Buscar",
+ "search.share": "Compartilhar",
+ "search.reset": "Limpar",
+ "search.result.initializer": "Inicializando busca",
+ "search.result.placeholder": "Digite para iniciar a busca",
+ "search.result.none": "Nenhum documento encontrado",
+ "search.result.one": "1 documento encontrado",
+ "search.result.other": "# documentos encontrados",
+ "search.result.more.one": "mais 1 nesta página",
+ "search.result.more.other": "# mais nesta página",
+ "search.result.term.missing": "Ausente",
+ "select.language": "Selecione o idioma",
+ "select.version": "Selecione a versão",
+ "source": "Ir para repositório",
+ "source.file.contributors": "Contribuidores",
+ "source.file.date.created": "Criado em",
+ "source.file.date.updated": "Última atualização",
+ "tabs": "Abas",
+ "toc": "Índice",
+ "top": "Voltar para o topo"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/pt.html b/src/templates/partials/languages/pt.html
new file mode 100644
index 00000000..e5dee1cb
--- /dev/null
+++ b/src/templates/partials/languages/pt.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Portuguese -->
+{% macro t(key) %}{{ {
+ "language": "pt",
+ "action.edit": "Editar esta página",
+ "action.skip": "Ir para o conteúdo",
+ "action.view": "Ver fonte desta página",
+ "announce.dismiss": "Não mostrar novamente",
+ "blog.archive": "Arquivo",
+ "blog.categories": "Categorias",
+ "blog.categories.in": "em",
+ "blog.continue": "Continuar leitura",
+ "blog.draft": "Rascunho",
+ "blog.index": "Voltar ao índice",
+ "blog.meta": "Metadados",
+ "blog.references": "Ligações relacionadas",
+ "clipboard.copy": "Copiar para área de transferência",
+ "clipboard.copied": "Copiado para área de transferência",
+ "consent.accept": "Aceitar",
+ "consent.manage": "Gerir configurações",
+ "consent.reject": "Rejeitar",
+ "footer": "Rodapé",
+ "footer.next": "Próximo",
+ "footer.previous": "Anterior",
+ "header": "Cabeçalho",
+ "meta.comments": "Comentários",
+ "meta.source": "Fonte",
+ "nav": "Navegação",
+ "readtime.one": "1 min de leitura",
+ "readtime.other": "# min de leitura",
+ "rss.created": "canal RSS",
+ "rss.updated": "canal RSS com conteúdo atualizado",
+ "search": "Pesquisar",
+ "search.config.lang": "pt",
+ "search.placeholder": "Buscar",
+ "search.share": "Compartilhar",
+ "search.reset": "Limpar",
+ "search.result.initializer": "Inicializando a pesquisa",
+ "search.result.placeholder": "Digite para iniciar a busca",
+ "search.result.none": "Nenhum resultado encontrado",
+ "search.result.one": "1 resultado encontrado",
+ "search.result.other": "# resultados encontrados",
+ "search.result.more.one": "Mais 1 nesta página",
+ "search.result.more.other": "Mais # nesta página",
+ "search.result.term.missing": "Ausente",
+ "select.language": "Selecione o idioma",
+ "select.version": "Selecione a versão",
+ "source": "Ir ao repositório",
+ "source.file.contributors": "Colaboradores",
+ "source.file.date.created": "Criada",
+ "source.file.date.updated": "Última atualização",
+ "tabs": "Abas",
+ "toc": "Índice",
+ "top": "Voltar ao topo"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ro.html b/src/templates/partials/languages/ro.html
new file mode 100644
index 00000000..7bea9afb
--- /dev/null
+++ b/src/templates/partials/languages/ro.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Romanian -->
+{% macro t(key) %}{{ {
+ "language": "ro",
+ "action.edit": "Editeaza această pagină",
+ "action.skip": "Sari la conținut",
+ "action.view": "Vezi sursa acestei pagini",
+ "announce.dismiss": "Nu mai arăta asta",
+ "blog.archive": "Arhivează",
+ "blog.categories": "Categorii",
+ "blog.categories.in": "în",
+ "blog.continue": "Continuă să citești",
+ "blog.draft": "Ciornă",
+ "blog.index": "Înapoi la index",
+ "blog.meta": "Metadata",
+ "blog.references": "Link-uri relevante",
+ "clipboard.copy": "Copiază în clipboard",
+ "clipboard.copied": "Copiat în clipboard",
+ "consent.accept": "Accept",
+ "consent.manage": "Gestionați setările",
+ "consent.reject": "Refuz",
+ "footer": "Subsol",
+ "footer.next": "Următor",
+ "footer.previous": "Anterior",
+ "header": "Antet",
+ "meta.comments": "Comentarii",
+ "meta.source": "Sursă",
+ "nav": "Navigație",
+ "readtime.one": "1 minut de citit",
+ "readtime.other": "# minut de citit",
+ "rss.created": "Flux RSS",
+ "rss.updated": "Flux RSS cu conținut actualizat",
+ "search": "Caută",
+ "search.config.lang": "ro",
+ "search.placeholder": "Căutare",
+ "search.share": "Distribuie",
+ "search.reset": "Resetează",
+ "search.result.initializer": "Inițializare căutare",
+ "search.result.placeholder": "Tastează pentru a începe căutarea",
+ "search.result.none": "Nu a fost găsit niciun document",
+ "search.result.one": "1 document găsit",
+ "search.result.other": "# documente găsite",
+ "search.result.more.one": "Încă 1 pe această pagină",
+ "search.result.more.other": "Încă # pe această pagină",
+ "search.result.term.missing": "Lipsă",
+ "select.language": "Selectează limba",
+ "select.version": "Selectează versuine",
+ "source": "Accesează repository-ul",
+ "source.file.contributors": "Contribuitori",
+ "source.file.date.created": "Creată",
+ "source.file.date.updated": "Ultima actualizare",
+ "tabs": "File",
+ "toc": "Cuprins",
+ "top": "Înapoi sus"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ru.html b/src/templates/partials/languages/ru.html
new file mode 100644
index 00000000..ddbd7b95
--- /dev/null
+++ b/src/templates/partials/languages/ru.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Russian -->
+{% macro t(key) %}{{ {
+ "language": "ru",
+ "action.edit": "Редактировать страницу",
+ "action.skip": "Перейти к содержанию",
+ "action.view": "Посмотреть исходный код страницы",
+ "announce.dismiss": "Больше не показывать",
+ "blog.archive": "Архив",
+ "blog.categories": "Категории",
+ "blog.categories.in": "В",
+ "blog.continue": "Читать",
+ "blog.draft": "Черновик",
+ "blog.index": "На главную",
+ "blog.meta": "Метаданные",
+ "blog.references": "Ссылки",
+ "clipboard.copy": "Копировать в буфер",
+ "clipboard.copied": "Скопировано в буфер",
+ "consent.accept": "Принять",
+ "consent.manage": "Управлять настройками",
+ "consent.reject": "Отклонить",
+ "footer": "Нижний колонтитул",
+ "footer.next": "Вперед",
+ "footer.previous": "Назад",
+ "header": "Верхний колонтитул",
+ "meta.comments": "Комментарии",
+ "meta.source": "Исходный код",
+ "nav": "Навигация",
+ "readtime.one": "Читать 1 минуту",
+ "readtime.other": "Читать # минут",
+ "rss.created": "RSS канал",
+ "rss.updated": "RSS канал с новым контентом",
+ "search": "Поиск",
+ "search.config.lang": "ru",
+ "search.placeholder": "Поиск",
+ "search.share": "Поделиться",
+ "search.reset": "Очистить",
+ "search.result.initializer": "Инициализация поиска",
+ "search.result.placeholder": "Начните печатать для поиска",
+ "search.result.none": "Совпадений не найдено",
+ "search.result.one": "Найдено 1 совпадение",
+ "search.result.other": "Найдено совпадений: #",
+ "search.result.more.one": "Ещё 1 на этой странице",
+ "search.result.more.other": "Ещё # на этой странице",
+ "search.result.term.missing": "Отсутствует",
+ "select.language": "Выберите язык",
+ "select.version": "Выберите версию",
+ "source": "Перейти к репозиторию",
+ "source.file.contributors": "Участники",
+ "source.file.date.created": "Дата создания",
+ "source.file.date.updated": "Последнее обновление",
+ "tabs": "Вкладки",
+ "toc": "Содержание",
+ "top": "К началу"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/sa.html b/src/templates/partials/languages/sa.html
new file mode 100644
index 00000000..338e2b61
--- /dev/null
+++ b/src/templates/partials/languages/sa.html
@@ -0,0 +1,75 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Sanskrit -->
+{% macro t(key) %}{{ {
+ "language": "sa",
+ "action.edit": "एतत् पृष्ठं सम्पादयतु",
+ "action.skip": "सामग्रीं त्यजन्तु",
+ "action.view": "अस्य पृष्ठस्य स्रोतः पश्यन्तु",
+ "announce.dismiss": "एतत् पुनः न दर्शयतु",
+ "blog.archive": "लेखागार",
+ "blog.categories": "श्रेणियाँ",
+ "blog.categories.in": "इत्यस्मिन्‌",
+ "blog.continue": "पठनं निरन्तरं कुर्वन्तु",
+ "blog.draft": "प्रारूप",
+ "blog.index": "अनुक्रमणिकां प्रति पुनः आगच्छन्तु",
+ "blog.meta": "परिदत्तांश",
+ "blog.references": "सन्दर्भाः",
+ "clipboard.copy": "एतत् प्रतिलिख्यताम्",
+ "clipboard.copied": "प्रतिलिपितः भवति",
+ "consent.accept": "अहं तत् स्वीकुर्वन् अस्मि",
+ "consent.manage": "वविन्यासं प्रबन्धयन्तु",
+ "consent.reject": "अहं तत् निराकरोमि",
+ "footer": "पादलेखः",
+ "footer.next": "अग्रिमः",
+ "footer.previous": "पूर्वकृत",
+ "header": "शीर्षकम्",
+ "meta.comments": "विचाराः",
+ "meta.source": "स्रोतः",
+ "nav": "मार्गदर्शनम्",
+ "readtime.one": "१ निमेषं पठितुं",
+ "readtime.other": "# निमेषं पठितुं",
+ "rss.created": "आरएसएस सेवा",
+ "rss.updated": "आरएसएस सेवातः नवीनतमं अद्यतनम्",
+ "search": "अन्वेषण",
+ "search.placeholder": "अन्वेषण",
+ "search.share": "भजतु",
+ "search.reset": "तत् स्वच्छं कुर्वन्तु",
+ "search.result.initializer": "अन्वेषणस्य आरम्भः",
+ "search.result.placeholder": "अन्वेषणं आरभ्य लिखन्तु",
+ "search.result.none": "अभिलेखाः नास्ति",
+ "search.result.one": "१ अभिलेखः अस्ति",
+ "search.result.other": "# अभिलेखाः सन्ति",
+ "search.result.more.one": "अस्मिन् पृष्ठे १ अन्यः अस्ति",
+ "search.result.more.other": "अस्मिन् पृष्ठे # अन्ये सन्ति",
+ "search.result.term.missing": "शून्य",
+ "select.language": "भाषां चिनोतु",
+ "select.version": "संस्करणं चिनोतु",
+ "source": "भण्डारं गच्छन्तु",
+ "source.file.contributors": "अंशदाता",
+ "source.file.date.created": "ननिर्माणस्य तिथिः",
+ "source.file.date.updated": "परिवर्तनस्य तिथिः",
+ "tabs": "पट्टाः",
+ "toc": "सामग्रीसारणी",
+ "top": "पुनः उपरिभागं प्रति गच्छन्तु"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/sh.html b/src/templates/partials/languages/sh.html
new file mode 100644
index 00000000..42a0d902
--- /dev/null
+++ b/src/templates/partials/languages/sh.html
@@ -0,0 +1,70 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Serbo-Croatian -->
+{% macro t(key) %}{{ {
+ "language": "sh",
+ "action.edit": "Ažuriraj stranicu",
+ "action.skip": "Idi na tekst",
+ "action.view": "Pogledaj izvorni kod ove stranice",
+ "announce.dismiss": "Nemoj mi ponovo pokazati ovo",
+ "blog.archive": "Arhiva",
+ "blog.categories": "Kategorije",
+ "blog.categories.in": "u",
+ "blog.continue": "Nastavi sa čitanjem",
+ "blog.meta": "Metapodaci",
+ "blog.references": "Povezani linkovi",
+ "clipboard.copy": "Kopiraj u klipbord",
+ "clipboard.copied": "Iskopirano u klipbord",
+ "consent.accept": "Prihvati",
+ "consent.manage": "Promeni podešavanja",
+ "consent.reject": "Odbij",
+ "footer": "Podnožje",
+ "footer.next": "Sledeće",
+ "footer.previous": "Prethodno",
+ "header": "Zaglavlje",
+ "meta.comments": "Komentari",
+ "meta.source": "Izvor",
+ "nav": "Navigacija",
+ "readtime.one": "1 minut čitanja",
+ "readtime.other": "# minuta čitanja",
+ "search": "Pretraga",
+ "search.placeholder": "Pretraga",
+ "search.share": "Deljenje",
+ "search.reset": "Očisti",
+ "search.result.initializer": "Inicijalizujem pretragu",
+ "search.result.placeholder": "Unesite pojam pretrage",
+ "search.result.none": "Ništa nije pronađeno",
+ "search.result.one": "1 rezultat pretrage",
+ "search.result.other": "# rezultata pretrage",
+ "search.result.more.one": "još 1 na ovoj strani",
+ "search.result.more.other": "još # na ovoj strani",
+ "search.result.term.missing": "Nedostaje",
+ "select.language": "Izaberi jezik",
+ "select.version": "Izaberi verziju",
+ "source": "Idi u repozitorijum",
+ "source.file.date.created": "Kreiran",
+ "source.file.date.updated": "Ažuriran",
+ "tabs": "Tabovi",
+ "toc": "Sadržaj",
+ "top": "Nazad na vrh"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/si.html b/src/templates/partials/languages/si.html
new file mode 100644
index 00000000..eb41309e
--- /dev/null
+++ b/src/templates/partials/languages/si.html
@@ -0,0 +1,51 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Sinhalese -->
+{% macro t(key) %}{{ {
+ "language": "si",
+ "action.edit": "පිටුව සංස්කරණය",
+ "action.skip": "අන්තර්ගතය වෙත යන්න",
+ "clipboard.copy": "කොපි කරන්න",
+ "clipboard.copied": "කොපි කළා",
+ "footer": "පාදම",
+ "footer.next": "මීළඟ",
+ "footer.previous": "පසුගිය",
+ "header": "ශීර්ෂය",
+ "meta.comments": "ප්‍රතිචාර",
+ "meta.source": "මූලාශ්‍රය",
+ "nav": "යාත්‍රණය",
+ "search.config.pipeline": " ",
+ "search.placeholder": "සොයන්න",
+ "search.reset": "මකන්න",
+ "search.result.placeholder": "සෙවීමට ටයිප් කරන්න",
+ "search.result.none": "කිසිවක් හමු නොවුණි",
+ "search.result.one": "1 ගැලපෙන ගොනුවක්",
+ "search.result.other": "ගැලපෙන ගොනු # ක්",
+ "search.result.more.one": "තව 1 ප්‍රතිඵලයක්",
+ "search.result.more.other": "තව ප්‍රතිඵල # ක්",
+ "source": "රිපොසිටරියට යන්න",
+ "source.file.date.created": "ٺاھيو ويو",
+ "source.file.date.updated": "අවසන් යාවත්කාලීන වීම",
+ "tabs": "ටැබ්ස්",
+ "toc": "පටුන"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/sk.html b/src/templates/partials/languages/sk.html
new file mode 100644
index 00000000..701a5a53
--- /dev/null
+++ b/src/templates/partials/languages/sk.html
@@ -0,0 +1,43 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Slovak -->
+{% macro t(key) %}{{ {
+ "language": "sk",
+ "action.edit": "Upraviť túto stránku",
+ "action.skip": "Preskočiť na obsah",
+ "clipboard.copy": "Kopírovať do schránky",
+ "clipboard.copied": "Skopírované do schránky",
+ "footer.next": "Ďalej",
+ "footer.previous": "Späť",
+ "meta.comments": "Komentáre",
+ "meta.source": "Zdroj",
+ "search.placeholder": "Hľadať",
+ "search.result.placeholder": "Pre vyhľadávanie začni písať",
+ "search.result.none": "Žiadne vyhovujúce dokumenty",
+ "search.result.one": "Vyhovujúci dokument: 1",
+ "search.result.other": "Vyhovujúce dokumenty: #",
+ "source": "Zobraziť repozitár",
+ "source.file.date.created": "Vytvorené",
+ "source.file.date.updated": "Posledná aktualizácia",
+ "toc": "Obsah"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/sl.html b/src/templates/partials/languages/sl.html
new file mode 100644
index 00000000..a01f31d9
--- /dev/null
+++ b/src/templates/partials/languages/sl.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Slovenian -->
+{% macro t(key) %}{{ {
+ "language": "sl",
+ "action.edit": "Uredi stran",
+ "action.skip": "Skoči na vsebino",
+ "action.view": "Prikaži izvorno stran",
+ "announce.dismiss": "Ne prikaži več",
+ "blog.archive": "Arhiv",
+ "blog.categories": "Kategorije",
+ "blog.categories.in": "v",
+ "blog.continue": "Nadaljuj z branjem",
+ "blog.draft": "Osnutek",
+ "blog.index": "Nazaj na kazalo",
+ "blog.meta": "Metapodatki",
+ "blog.references": "Sorodne povezave",
+ "clipboard.copy": "Kopiraj v odložišče",
+ "clipboard.copied": "Kopirano v odložišče",
+ "consent.accept": "Sprejmi",
+ "consent.manage": "Uredi nastavitve",
+ "consent.reject": "Zavrni",
+ "footer": "Glava",
+ "footer.next": "Naslednja stran",
+ "footer.previous": "Prejšnja stran",
+ "header": "Noga",
+ "meta.comments": "Komentarji",
+ "meta.source": "Izvorna koda",
+ "nav": "Navigacija",
+ "readtime.one": "Čas branja: 1 min",
+ "readtime.other": "Čas branja: # min",
+ "rss.created": "RSS vir",
+ "rss.updated": "RSS vir posodobljene vsebine",
+ "search": "Iskanje",
+ "search.config.lang": "sl",
+ "search.placeholder": "Išči",
+ "search.share": "Deli",
+ "search.reset": "Počisti",
+ "search.result.initializer": "Inicializacija iskanja",
+ "search.result.placeholder": "Vpiši iskalni niz",
+ "search.result.none": "Ni zadetkov",
+ "search.result.one": "1 zadetek",
+ "search.result.other": "# zadetkov",
+ "search.result.more.one": "Še 1 na tej strani",
+ "search.result.more.other": "Še # na tej strani",
+ "search.result.term.missing": "Manjka",
+ "select.language": "Izberi jezik",
+ "select.version": "Izberi različico",
+ "source": "Pojdi na repozitorij",
+ "source.file.contributors": "Soavtorji",
+ "source.file.date.created": "Ustvarjeno",
+ "source.file.date.updated": "Zadnja posodobitev",
+ "tabs": "Zavihki",
+ "toc": "Kazalo",
+ "top": "Nazaj na vrh"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/sr.html b/src/templates/partials/languages/sr.html
new file mode 100644
index 00000000..275ea126
--- /dev/null
+++ b/src/templates/partials/languages/sr.html
@@ -0,0 +1,57 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Serbian -->
+{% macro t(key) %}{{ {
+ "language": "sr",
+ "action.edit": "Ажурирај страницу",
+ "action.skip": "Иди на текст",
+ "clipboard.copy": "Копирај у клипборд",
+ "clipboard.copied": "Ископирано у клипборд",
+ "footer": "Подножје",
+ "footer.next": "Следеће",
+ "footer.previous": "Претходно",
+ "header": "Заглавље",
+ "meta.comments": "Коментари",
+ "meta.source": "Извор",
+ "nav": "Навигација",
+ "search": "Претрага",
+ "search.placeholder": "Претрага",
+ "search.share": "Дељење",
+ "search.reset": "Очисти",
+ "search.result.initializer": "Иницијализујем претрагу",
+ "search.result.placeholder": "Унесите појам претраге",
+ "search.result.none": "Ништа није пронађено",
+ "search.result.one": "1 резултат претраге",
+ "search.result.other": "# резултата претраге",
+ "search.result.more.one": "још 1 на овој страни",
+ "search.result.more.other": "још # на овој страни",
+ "search.result.term.missing": "Недостаје",
+ "select.language": "Изабери језик",
+ "select.version": "Изабери верзију",
+ "source": "Иди у репозиторијум",
+ "source.file.date.created": "Креиран",
+ "source.file.date.updated": "Ажуриран",
+ "tabs": "Табови",
+ "toc": "Садржај",
+ "top": "Назад на врх"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/sv.html b/src/templates/partials/languages/sv.html
new file mode 100644
index 00000000..52f151d2
--- /dev/null
+++ b/src/templates/partials/languages/sv.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Swedish -->
+{% macro t(key) %}{{ {
+ "language": "sv",
+ "action.edit": "Redigera sidan",
+ "action.skip": "Gå till innehållet",
+ "action.view": "Visa källkoden för denna sida",
+ "announce.dismiss": "Visa inte igen",
+ "blog.archive": "Arkivera",
+ "blog.categories": "Kategorier",
+ "blog.categories.in": "i",
+ "blog.continue": "Fortsätt läsa",
+ "blog.draft": "Utkast",
+ "blog.index": "Tillbaka till index",
+ "blog.meta": "Metadata",
+ "blog.references": "Relaterade länkar",
+ "clipboard.copy": "Kopiera till urklipp",
+ "clipboard.copied": "Kopierat till urklipp",
+ "consent.accept": "Acceptera",
+ "consent.manage": "Hantera inställningar",
+ "consent.reject": "Acceptera inte",
+ "footer": "Sidfot",
+ "footer.next": "Nästa",
+ "footer.previous": "Föregående",
+ "header": "Sidhuvud",
+ "meta.comments": "Kommentarer",
+ "meta.source": "Källa",
+ "nav": "Navigation",
+ "readtime.one": "1 min lästid",
+ "readtime.other": "# min lästid",
+ "rss.created": "RSS-flöde",
+ "rss.updated": "RSS-flöde av uppdaterat innehåll",
+ "search": "Sök",
+ "search.config.lang": "sv",
+ "search.placeholder": "Sök",
+ "search.share": "Dela",
+ "search.reset": "Rensa",
+ "search.result.initializer": "Initialiserar sök",
+ "search.result.placeholder": "Skriv sökord",
+ "search.result.none": "Inga sökresultat",
+ "search.result.one": "1 sökresultat",
+ "search.result.other": "# sökresultat",
+ "search.result.more.one": "1 till på denna sida",
+ "search.result.more.other": "# till på denna sida",
+ "search.result.term.missing": "Saknas",
+ "select.language": "Välj språk",
+ "select.version": "Välj version",
+ "source": "Gå till datakatalog",
+ "source.file.contributors": "Författare",
+ "source.file.date.created": "Skapad",
+ "source.file.date.updated": "Senast uppdaterad",
+ "tabs": "Flikar",
+ "toc": "Innehållsförteckning",
+ "top": "Tillbaka till toppen"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/te.html b/src/templates/partials/languages/te.html
new file mode 100644
index 00000000..7529a47c
--- /dev/null
+++ b/src/templates/partials/languages/te.html
@@ -0,0 +1,75 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Telugu -->
+{% macro t(key) %}{{ {
+ "language": "te",
+ "action.edit": "ఈ పేజీలో దిద్దుబాట్లు చేయండి",
+ "action.skip": "సమాచారానికి వెళ్లండి",
+ "action.view": "నేను ఈ పేజీ యొక్క మూలాన్ని చూడాలనుకుంటున్నాను",
+ "announce.dismiss": "దీన్ని మళ్లీ చూపవద్దు",
+ "blog.archive": "పాత వ్యాసం",
+ "blog.categories": "వర్గాలు",
+ "blog.categories.in": "లో",
+ "blog.continue": "చదవడం కొనసాగించండి",
+ "blog.draft": "ప్రారంభ రచన",
+ "blog.index": "సూచికకు తిరిగి వెళ్ళు",
+ "blog.meta": "సమాచారం గురించి సమాచారం",
+ "blog.references": "సంబంధిత సూచనలు",
+ "clipboard.copy": "దీనిని అనుకరించు",
+ "clipboard.copied": "దీనిని అతికించు",
+ "consent.accept": "నేను దీనిని అంగీకరిస్తున్నాను",
+ "consent.manage": "ఆకృతీకరణను నిర్వహించండి",
+ "consent.reject": "నేను దీనిని తిరస్కరిస్తున్నాను",
+ "footer": "అడిటిప్పణి",
+ "footer.next": "తదుపరి భాగం",
+ "footer.previous": "మునుపటి భాగం",
+ "header": "శీర్షిక విభాగం",
+ "meta.comments": "అభిప్రాయాలు",
+ "meta.source": "మూలం",
+ "nav": "మార్గదర్శక పట్టీ",
+ "readtime.one": "చదవడానికి ఒక నిమిషం పడుతుంది",
+ "readtime.other": "చదవడానికి # నిమిషాలు పడుతుంది",
+ "rss.created": "ఆర్ఎస్ఎస్ సేవ",
+ "rss.updated": "ఆర్ఎస్ఎస్ సేవ నుండి తాజా నవీకరణ",
+ "search": "వెతకండి",
+ "search.placeholder": "వెతకండి",
+ "search.share": "పంచుకోండి",
+ "search.reset": "తుడిచివేయు",
+ "search.result.initializer": "శోధనను ప్రారంభిస్తోంది",
+ "search.result.placeholder": "రాయడం ద్వారా వెతకడం ప్రారంభించండి",
+ "search.result.none": "సరిపోలే పత్రాలు లేవు",
+ "search.result.one": "ఒక సరిపోలే పత్రం",
+ "search.result.other": "# సరిపోలే పత్రాలు",
+ "search.result.more.one": "ఈ పేజీలో మరొకటి",
+ "search.result.more.other": "ఈ పేజీలో ఇంకా # ఉన్నాయి",
+ "search.result.term.missing": "తప్పిపోయింది",
+ "select.language": "భాషను ఎంచుకోండి",
+ "select.version": "సంస్కరణను ఎంచుకోండి",
+ "source": "భండారానికి వెళ్ళండి",
+ "source.file.contributors": "సహకారులు",
+ "source.file.date.created": "సృష్టించబడింది",
+ "source.file.date.updated": "చివరి నవీకరణ",
+ "tabs": "వివిధ కిటికీలు",
+ "toc": "విషయ సూచిక",
+ "top": "పైకి తిరిగి వెళ్ళు"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/th.html b/src/templates/partials/languages/th.html
new file mode 100644
index 00000000..c8104fc1
--- /dev/null
+++ b/src/templates/partials/languages/th.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Thai -->
+{% macro t(key) %}{{ {
+ "language": "th",
+ "action.edit": "แก้ไขหน้านี้",
+ "action.skip": "ข้ามไปที่เนื้อหา",
+ "action.view": "ดูแหล่งที่มาของหน้านี้",
+ "announce.dismiss": "อย่าแสดงสิ่งนี้อีก",
+ "blog.archive": "คลังเก็บเอกสาร",
+ "blog.categories": "หมวดหมู่",
+ "blog.categories.in": "ใย",
+ "blog.continue": "อ่านต่อไป",
+ "blog.draft": "ฉบับร่าง",
+ "blog.index": "กลับไปยังหน้าแรก",
+ "blog.meta": "คำอธิบายข้อมูล",
+ "blog.references": "ลิงก์ที่เกี่ยวข้อง",
+ "clipboard.copy": "คัดลอก",
+ "clipboard.copied": "คัดลอกแล้ว",
+ "consent.accept": "ยอมรับ",
+ "consent.manage": "จัดการการตั้งค่า",
+ "consent.reject": "ปฏิเสธ",
+ "footer": "ส่วนท้าย",
+ "footer.next": "ต่อไป",
+ "footer.previous": "ก่อนหน้า",
+ "header": "หัวข้อ",
+ "meta.comments": "ความคิดเห็น",
+ "meta.source": "แหล่งที่มา",
+ "nav": "ตัวนำทาง",
+ "readtime.one": "อ่าน 1 นาที",
+ "readtime.other": "อ่าน # นาที",
+ "rss.created": "ฟีด RSS",
+ "rss.updated": "ฟีด RSS ของเนื้อหาที่อัปเดต",
+ "search": "ค้นหา",
+ "search.config.lang": "th",
+ "search.placeholder": "ค้นหา",
+ "search.share": "แบ่งปัน",
+ "search.reset": "ล้าง",
+ "search.result.initializer": "กำลังเริ่มต้นการค้นหา",
+ "search.result.placeholder": "พิมพ์เพื่อเริ่มค้นหา",
+ "search.result.none": "ไม่พบเอกสารที่ตรงกัน",
+ "search.result.one": "พบเอกสารที่ตรงกัน",
+ "search.result.other": "พบ # เอกสารที่ตรงกัน",
+ "search.result.more.one": "อีกหนึ่งในหน้านี้",
+ "search.result.more.other": "# เพิ่มเติมในหน้านี้",
+ "search.result.term.missing": "ไม่พบ",
+ "select.language": "เลือกภาษา",
+ "select.version": "เลือกเวอร์ชัน",
+ "source": "ไปที่พื้นที่เก็บข้อมูล",
+ "source.file.contributors": "ผู้มีส่วนร่วม",
+ "source.file.date.created": "สร้าง",
+ "source.file.date.updated": "สร้าง",
+ "tabs": "แท็บ",
+ "toc": "สารบัญ",
+ "top": "กลับไปด้านบนสุด"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/tl.html b/src/templates/partials/languages/tl.html
new file mode 100644
index 00000000..00c22c99
--- /dev/null
+++ b/src/templates/partials/languages/tl.html
@@ -0,0 +1,57 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Tagalog -->
+{% macro t(key) %}{{ {
+ "language": "tl",
+ "action.edit": "I-edit ang pahinang ito",
+ "action.skip": "I-skip tungo sa nilalaman",
+ "clipboard.copy": "Kopyahin sa clipboard",
+ "clipboard.copied": "Nakopya mula sa clipboard",
+ "footer": "Lagdang Pangwakas",
+ "footer.next": "Susunod",
+ "footer.previous": "Nakaraan",
+ "header": "Pamuhatan",
+ "meta.comments": "Mga Komento",
+ "meta.source": "Pinagmulan",
+ "nav": "Nabigasyon",
+ "search": "Hanapin",
+ "search.placeholder": "Hanapin",
+ "search.share": "Ibahagi",
+ "search.reset": "Tanggalin",
+ "search.result.initializer": "Sinisimulan ang paghahanap",
+ "search.result.placeholder": "Mag-type upang simulan ang paghahanap",
+ "search.result.none": "Walang nahanap na dokumento",
+ "search.result.one": "1 magkatugmang dokumento",
+ "search.result.other": "# magkatugmang mga dokumento",
+ "search.result.more.one": "1 meron sa pahina na ito",
+ "search.result.more.other": "# meron sa pahina na ito",
+ "search.result.term.missing": "Nawawala",
+ "select.language": "Pumili ng lenguwahe",
+ "select.version": "Pumili ng bersyon",
+ "source": "Pumunta sa repository",
+ "source.file.date.created": "Nagawa",
+ "source.file.date.updated": "Huling update",
+ "tabs": "Mga tala",
+ "toc": "Talaan ng nilalaman",
+ "top": "Bumalik sa taas"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/tr.html b/src/templates/partials/languages/tr.html
new file mode 100644
index 00000000..860f8ed7
--- /dev/null
+++ b/src/templates/partials/languages/tr.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Turkish -->
+{% macro t(key) %}{{ {
+ "language": "tr",
+ "action.edit": "Düzenle",
+ "action.skip": "Ana içeriğe geç",
+ "action.view": "Sayfanın kaynağını görüntüle",
+ "announce.dismiss": "Bir daha gösterme",
+ "blog.archive": "Arşiv",
+ "blog.categories": "Kategoriler",
+ "blog.categories.in": "in",
+ "blog.continue": "Okumaya devam et",
+ "blog.draft": "Taslak",
+ "blog.index": "Dizine geri dön",
+ "blog.meta": "Metadata",
+ "blog.references": "İlgili bağlantılar",
+ "clipboard.copy": "Kopyala",
+ "clipboard.copied": "Kopyalandı",
+ "consent.accept": "Kabul et",
+ "consent.manage": "Ayarları yönet",
+ "consent.reject": "Reddet",
+ "footer": "Altbilgi",
+ "footer.next": "Sonraki",
+ "footer.previous": "Önceki",
+ "header": "Başlık",
+ "meta.comments": "Yorumlar",
+ "meta.source": "Kaynak",
+ "nav": "Navigasyon",
+ "readtime.one": "1 dakika okuma",
+ "readtime.other": "# dakika okuma",
+ "rss.created": "RSS beslemesi",
+ "rss.updated": "Güncellenmiş içeriğin RSS beslemesi",
+ "search": "Ara",
+ "search.config.lang": "tr",
+ "search.placeholder": "Ara",
+ "search.share": "Paylaş",
+ "search.reset": "Temizle",
+ "search.result.initializer": "Arama başlatılıyor",
+ "search.result.placeholder": "Aramaya başlamak için yazın",
+ "search.result.none": "Eşleşen doküman bulunamadı",
+ "search.result.one": "1 doküman bulundu",
+ "search.result.other": "# doküman bulundu",
+ "search.result.more.one": "Bu sayfada 1 tane daha",
+ "search.result.more.other": "Bu sayfada # tane daha",
+ "search.result.term.missing": "Eksik",
+ "select.language": "Dil seç",
+ "select.version": "Versiyon seç",
+ "source": "Depoya git",
+ "source.file.contributors": "Katkıda bulunanlar",
+ "source.file.date.created": "Oluşturuldu",
+ "source.file.date.updated": "Son Güncelleme",
+ "tabs": "Sekmeler",
+ "toc": "İçindekiler",
+ "top": "Başa dön"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/uk.html b/src/templates/partials/languages/uk.html
new file mode 100644
index 00000000..ca5c709c
--- /dev/null
+++ b/src/templates/partials/languages/uk.html
@@ -0,0 +1,75 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Ukrainian -->
+{% macro t(key) %}{{ {
+ "language": "uk",
+ "action.edit": "Редагувати сторінку",
+ "action.skip": "Перейти до змісту",
+ "action.view": "Переглянути вихідний код сторінки",
+ "announce.dismiss": "Більше не показувати",
+ "blog.archive": "Архівувати",
+ "blog.categories": "Категорії",
+ "blog.categories.in": "в",
+ "blog.continue": "Читати далі",
+ "blog.draft": "Чернетка",
+ "blog.index": "Повернутись на головну",
+ "blog.meta": "Метадані",
+ "blog.references": "Пов'язані посилання",
+ "clipboard.copy": "Скопіювати в буфер",
+ "clipboard.copied": "Скопійовано в буфер",
+ "consent.accept": "Прийняти",
+ "consent.manage": "Керувати налаштуваннями",
+ "consent.reject": "Відхилити",
+ "footer": "Футер",
+ "footer.next": "Вперед",
+ "footer.previous": "Назад",
+ "header": "Хедер",
+ "meta.comments": "Коментарі",
+ "meta.source": "Вихідний код",
+ "nav": "Навігація",
+ "readtime.one": "Час на прочитання: 1 хвилина",
+ "readtime.other": "Час на прочитання: # хвилин",
+ "rss.created": "RSS стрічка",
+ "rss.updated": "RSS стрічка оновленого контенту",
+ "search": "Шукати",
+ "search.placeholder": "Пошук",
+ "search.share": "Поділитись",
+ "search.reset": "Очистити",
+ "search.result.initializer": "Пошук розпочато",
+ "search.result.placeholder": "Розпочніть писати для пошуку",
+ "search.result.none": "Збігів не знайдено",
+ "search.result.one": "Знайдено 1 збіг",
+ "search.result.other": "Знайдено # збігів",
+ "search.result.more.one": "Ще 1 збіг на цій сторінці",
+ "search.result.more.other": "Ще # збігів на цій сторінці",
+ "search.result.term.missing": "Не знайдено запиту",
+ "select.language": "Обрати мову",
+ "select.version": "Обрати версію",
+ "source": "Перейти до вихідного коду",
+ "source.file.contributors": "Контриб'ютори",
+ "source.file.date.created": "Створено",
+ "source.file.date.updated": "Востаннє оновлено",
+ "tabs": "Вкладки",
+ "toc": "Зміст",
+ "top": "Повернутись нагору"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/ur.html b/src/templates/partials/languages/ur.html
new file mode 100644
index 00000000..14a50588
--- /dev/null
+++ b/src/templates/partials/languages/ur.html
@@ -0,0 +1,77 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Urdu -->
+{% macro t(key) %}{{ {
+ "language": "ur",
+ "direction": "rtl",
+ "action.edit": "اس صفحے میں ترمیم کریں",
+ "action.skip": "براہِ راست مواد پر جائیں",
+ "action.view": "اس صفحہ کا ماخذ دیکھیں",
+ "announce.dismiss": "اسے دوبارہ مت دکھائیں",
+ "blog.archive": "محفوظ شدہ",
+ "blog.categories": "اقسام",
+ "blog.categories.in": "میں",
+ "blog.continue": "پڑھنا جاری رکھیے",
+ "blog.draft": "ڈرافٹ",
+ "blog.index": "واپس انڈیکس پر جائیں",
+ "blog.meta": "میٹا ڈیٹا",
+ "blog.references": "متعلقہ لنکس",
+ "clipboard.copy": "کلِپ بورڈ میں نقل کریں",
+ "clipboard.copied": "کلِپ بورڈ میں نقل کر دیا گیا",
+ "consent.accept": "قبول کریں",
+ "consent.manage": "سیٹینگ بدلیں",
+ "consent.reject": "رد کرنا",
+ "footer": "ذیلی تحریر",
+ "footer.next": "اگلا",
+ "footer.previous": "پچھلا",
+ "header": "سر تحریر",
+ "meta.comments": "تبصرے",
+ "meta.source": "ذریعہ",
+ "nav": "رہنمائی",
+ "readtime.one": "1 منٹ لگے گا",
+ "readtime.other": "# منٹ لگیں گے",
+ "rss.created": "RSS فیڈ",
+ "rss.updated": "تازہ ترین مواد کی RSS فیڈ",
+ "search": "تلاش",
+ "search.config.pipeline": " ",
+ "search.placeholder": "تلاش کریں",
+ "search.share": "اشتراک کریں",
+ "search.reset": "صاف کریں",
+ "search.result.initializer": "تلاش کا آغاز ہو رہا ہے",
+ "search.result.placeholder": "تلاش شروع کرنے کے لئے ٹائپ کریں",
+ "search.result.none": "کوئی ملتی جلتی دستاویزات نہیں",
+ "search.result.one": "۱ ملتی جلتی دستاویز",
+ "search.result.other": "# ملتی جلتی دستاویزات",
+ "search.result.more.one": "اِس صفحے پر مزید ۱",
+ "search.result.more.other": "اِس صفحے پر مزید #",
+ "search.result.term.missing": "گمشدہ",
+ "select.language": "زبان کا انتخاب کریں",
+ "select.version": "ورژن کا انتخاب کریں",
+ "source": "ریپازٹری پر جائیں",
+ "source.file.contributors": "تعاون کار",
+ "source.file.date.created": "تخلیق",
+ "source.file.date.updated": "آخری بار تجدید",
+ "tabs": "ٹیبز",
+ "toc": "فہرست",
+ "top": "واپس اوپر جائیں"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/uz.html b/src/templates/partials/languages/uz.html
new file mode 100644
index 00000000..d86f4db2
--- /dev/null
+++ b/src/templates/partials/languages/uz.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Uzbek -->
+{% macro t(key) %}{{ {
+ "language": "uz",
+ "action.edit": "Ushbu sahifani tahrirlash",
+ "action.skip": "Tarkibga o'tish",
+ "action.view": "Ushbu sahifaning manbasini ko'rish",
+ "announce.dismiss": "Buni boshqa ko'rsatma",
+ "blog.archive": "Arxiv",
+ "blog.categories": "Kategoriyalar",
+ "blog.categories.in": "ichida",
+ "blog.continue": "O'qishni davom ettiring",
+ "blog.draft": "Qoralama",
+ "blog.index": "Indeks sahifasiga qaytish",
+ "blog.meta": "Metama'lumot",
+ "blog.references": "Bog'liq havolalar",
+ "clipboard.copy": "Buferga nusxalash",
+ "clipboard.copied": "Buferga nusxalandi",
+ "consent.accept": "Qabul qilish",
+ "consent.manage": "Sozlamalarni boshqarish",
+ "consent.reject": "Rad etish",
+ "footer": "Pastgi qism",
+ "footer.next": "Keyingi sahifa",
+ "footer.previous": "Oldingi sahifa",
+ "header": "Sarlavha",
+ "meta.comments": "Izohlar",
+ "meta.source": "Manba",
+ "nav": "Navigatsiya",
+ "readtime.one": "1 daqiqa o'qish",
+ "readtime.other": "# daqiqa o'qish",
+ "rss.created": "RSS tasmasi",
+ "rss.updated": "Yangilangan kontentning RSS tasmasi",
+ "search": "Qidirish",
+ "search.config.lang": "tr",
+ "search.placeholder": "Qidirish",
+ "search.share": "Ulashish",
+ "search.reset": "Tozalash",
+ "search.result.initializer": "Qidiruv ishga tushirilmoqda",
+ "search.result.placeholder": "Qidiruvni boshlash uchun kiriting",
+ "search.result.none": "Mos natijalar yo'q",
+ "search.result.one": "1 ta mos natija",
+ "search.result.other": "# ta mos keladigan natijalar",
+ "search.result.more.one": "Ushbu sahifada yana 1 ta natija",
+ "search.result.more.other": "Bu sahifada yana # ta natija",
+ "search.result.term.missing": "To'ldirilmagan",
+ "select.language": "Tilni tanlang",
+ "select.version": "Versiyani tanlang",
+ "source": "Repozitoriyga o'tish",
+ "source.file.contributors": "Hissa qo'shuvchilar",
+ "source.file.date.created": "Yaratildi",
+ "source.file.date.updated": "Oxirgi yangilanish",
+ "tabs": "Yorliqlar",
+ "toc": "Mundarija",
+ "top": "Yuqoriga qaytish"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/vi.html b/src/templates/partials/languages/vi.html
new file mode 100644
index 00000000..b63a8d82
--- /dev/null
+++ b/src/templates/partials/languages/vi.html
@@ -0,0 +1,76 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Vietnamese -->
+{% macro t(key) %}{{ {
+ "language": "vi",
+ "action.edit": "Chỉnh sửa",
+ "action.skip": "Bỏ qua",
+ "action.view": "Xem mã nguồn của trang",
+ "announce.dismiss": "Không hiển thị lại",
+ "blog.archive": "Lưu trữ",
+ "blog.categories": "Mục",
+ "blog.categories.in": "Trong",
+ "blog.continue": "Tiếp tục đọc",
+ "blog.draft": "Bản nháp",
+ "blog.index": "Quay lại",
+ "blog.meta": "Metadata",
+ "blog.references": "Các liên kết liên quan",
+ "clipboard.copy": "Sao chép vào bộ nhớ tạm",
+ "clipboard.copied": "Đã sao chép",
+ "consent.accept": "Đồng ý",
+ "consent.manage": "Cài đặt",
+ "consent.reject": "Từ chối",
+ "footer": "Chân trang",
+ "footer.next": "Sau",
+ "footer.previous": "Trước",
+ "header": "Đầu trang",
+ "meta.comments": "Bình luận",
+ "meta.source": "Mã nguồn",
+ "nav": "Thanh điều hướng",
+ "readtime.one": "1 phút đọc",
+ "readtime.other": "# phút đọc",
+ "rss.created": "RSS feed",
+ "rss.updated": "RSS feed of updated content",
+ "search": "Tìm kiếm",
+ "search.config.lang": "vi",
+ "search.placeholder": "Tìm kiếm",
+ "search.share": "Chia sẻ",
+ "search.reset": "Xoá",
+ "search.result.initializer": "Initializing search",
+ "search.result.placeholder": "Nhập để bắt đầu tìm kiếm",
+ "search.result.none": "Không tìm thấy tài liệu liên quan",
+ "search.result.one": "1 tài liệu liên quan",
+ "search.result.other": "# tài liệu liên quan",
+ "search.result.more.one": "1 từ khác trong trang",
+ "search.result.more.other": "# từ khác trong trang",
+ "search.result.term.missing": "Không",
+ "select.language": "Chọn ngôn ngữ",
+ "select.version": "Chọn phiên bản",
+ "source": "Xem mã nguồn",
+ "source.file.contributors": "Contributors",
+ "source.file.date.created": "Tạo",
+ "source.file.date.updated": "Cập nhật lần cuối",
+ "tabs": "Tabs",
+ "toc": "Mục lục",
+ "top": "Trở lại mục lục"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/zh-Hant.html b/src/templates/partials/languages/zh-Hant.html
new file mode 100644
index 00000000..578fc82a
--- /dev/null
+++ b/src/templates/partials/languages/zh-Hant.html
@@ -0,0 +1,77 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Chinese (Traditional) -->
+{% macro t(key) %}{{ {
+ "language": "zh-Hant",
+ "action.edit": "編輯此頁",
+ "action.skip": "跳轉至",
+ "action.view": "查看源代碼",
+ "announce.dismiss": "不再顯示此訊息",
+ "blog.archive": "存檔",
+ "blog.categories": "分類",
+ "blog.categories.in": "分類在",
+ "blog.continue": "繼續閲讀",
+ "blog.draft": "草稿",
+ "blog.index": "回到首頁",
+ "blog.meta": "元數據",
+ "blog.references": "相關鏈接",
+ "clipboard.copy": "拷貝",
+ "clipboard.copied": "已拷貝",
+ "consent.accept": "接受",
+ "consent.manage": "管理設置",
+ "consent.reject": "拒絕",
+ "footer": "頁脚",
+ "footer.next": "下一頁",
+ "footer.previous": "上一頁",
+ "header": "頁首",
+ "meta.comments": "評論",
+ "meta.source": "來源",
+ "search.config.pipeline": "stemmer",
+ "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
+ "nav": "導航",
+ "readtime.one": "需要 1 分鐘閲讀",
+ "readtime.other": "需要 # 分鐘閲讀",
+ "rss.created": "簡易資訊聚合",
+ "rss.updated": "更新之部分的簡易資訊聚合",
+ "search": "搜尋",
+ "search.placeholder": "搜尋",
+ "search.share": "分享",
+ "search.reset": "清空",
+ "search.result.initializer": "正在初始化搜尋引擎",
+ "search.result.placeholder": "鍵入以開始檢索",
+ "search.result.none": "沒有找到符合條件的結果",
+ "search.result.one": "找到 1 个符合條件的結果",
+ "search.result.other": "找到 # 個符合條件的結果",
+ "search.result.more.one": "此頁尚有 1 個符合的項目",
+ "search.result.more.other": "此頁尚有 # 個符合的項目",
+ "search.result.term.missing": "缺失",
+ "select.language": "選擇語言",
+ "select.version": "選擇版本",
+ "source": "前往倉庫",
+ "source.file.contributors": "貢獻者",
+ "source.file.date.created": "建立日期",
+ "source.file.date.updated": "最後更新",
+ "tabs": "標籤頁",
+ "toc": "目錄",
+ "top": "回到頂部"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/zh-TW.html b/src/templates/partials/languages/zh-TW.html
new file mode 100644
index 00000000..405538f8
--- /dev/null
+++ b/src/templates/partials/languages/zh-TW.html
@@ -0,0 +1,77 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Chinese (Taiwanese) -->
+{% macro t(key) %}{{ {
+ "language": "zh-TW",
+ "action.edit": "編輯此頁",
+ "action.skip": "跳轉到",
+ "action.view": "查看此頁原始碼",
+ "announce.dismiss": "不再顯示此訊息",
+ "blog.archive": "封存",
+ "blog.categories": "分類",
+ "blog.categories.in": "於",
+ "blog.continue": "繼續閱讀",
+ "blog.draft": "草稿",
+ "blog.index": "回到主頁",
+ "blog.meta": "元數據",
+ "blog.references": "相關連結",
+ "clipboard.copy": "複製",
+ "clipboard.copied": "已複製",
+ "consent.accept": "同意",
+ "consent.manage": "管理設定",
+ "consent.reject": "拒絕",
+ "footer": "頁腳",
+ "footer.next": "下一頁",
+ "footer.previous": "上一頁",
+ "header": "頁首",
+ "meta.comments": "留言",
+ "meta.source": "來源",
+ "nav": "導覽列",
+ "readtime.one": "需要 1 分鐘閱讀時間",
+ "readtime.other": "需要 # 分鐘閱讀時間",
+ "rss.created": "RSS 訂閱",
+ "rss.updated": "RSS 訂閱內容已更新",
+ "search": "搜尋",
+ "search.config.pipeline": "stemmer",
+ "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
+ "search.placeholder": "搜尋",
+ "search.share": "分享",
+ "search.reset": "清除",
+ "search.result.initializer": "正在初始化搜尋引擎",
+ "search.result.placeholder": "打字進行搜尋",
+ "search.result.none": "沒有符合的項目",
+ "search.result.one": "找到 1 個符合的項目",
+ "search.result.other": "找到 # 個符合的項目",
+ "search.result.more.one": "此頁尚有 1 個符合的項目",
+ "search.result.more.other": "此頁尚有 # 個符合的項目",
+ "search.result.term.missing": "缺少字詞",
+ "select.language": "選擇語言",
+ "select.version": "選擇版本",
+ "source": "前往倉庫",
+ "source.file.contributors": "貢獻者",
+ "source.file.date.created": "建立日期",
+ "source.file.date.updated": "最後更新",
+ "tabs": "標籤",
+ "toc": "目錄",
+ "top": "回到頂端"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/languages/zh.html b/src/templates/partials/languages/zh.html
new file mode 100644
index 00000000..49f233a4
--- /dev/null
+++ b/src/templates/partials/languages/zh.html
@@ -0,0 +1,77 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Translations: Chinese (Simplified) -->
+{% macro t(key) %}{{ {
+ "language": "zh",
+ "action.edit": "编辑此页",
+ "action.skip": "跳转至",
+ "action.view": "查看本页的源代码",
+ "announce.dismiss": "不再显示此消息",
+ "blog.archive": "归档",
+ "blog.categories": "分类",
+ "blog.categories.in": "分类于",
+ "blog.continue": "继续阅读",
+ "blog.draft": "草稿",
+ "blog.index": "回到主页",
+ "blog.meta": "元数据",
+ "blog.references": "相关链接",
+ "clipboard.copy": "复制",
+ "clipboard.copied": "已复制",
+ "consent.accept": "同意",
+ "consent.manage": "管理设定",
+ "consent.reject": "拒绝",
+ "footer": "页脚",
+ "footer.next": "下一页",
+ "footer.previous": "上一页",
+ "header": "页眉",
+ "meta.comments": "评论",
+ "meta.source": "来源",
+ "nav": "导航栏",
+ "readtime.one": "需要 1 分钟阅读时间",
+ "readtime.other": "需要 # 分钟阅读时间",
+ "rss.created": "RSS 订阅",
+ "rss.updated": "已更新内容的 RSS 订阅",
+ "search": "查找",
+ "search.config.pipeline": "stemmer",
+ "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
+ "search.placeholder": "搜索",
+ "search.share": "分享",
+ "search.reset": "清空当前内容",
+ "search.result.initializer": "正在初始化搜索引擎",
+ "search.result.placeholder": "键入以开始搜索",
+ "search.result.none": "没有找到符合条件的结果",
+ "search.result.one": "找到 1 个符合条件的结果",
+ "search.result.other": "# 个符合条件的结果",
+ "search.result.more.one": "在该页上还有 1 个符合条件的结果",
+ "search.result.more.other": "在该页上还有 # 个符合条件的结果",
+ "search.result.term.missing": "缺少",
+ "select.language": "选择当前语言",
+ "select.version": "选择当前版本",
+ "source": "前往仓库",
+ "source.file.contributors": "贡献者",
+ "source.file.date.created": "创建日期",
+ "source.file.date.updated": "最后更新",
+ "tabs": "标签",
+ "toc": "目录",
+ "top": "回到页面顶部"
+}[key] }}{% endmacro %}
diff --git a/src/templates/partials/logo.html b/src/templates/partials/logo.html
new file mode 100644
index 00000000..05832c71
--- /dev/null
+++ b/src/templates/partials/logo.html
@@ -0,0 +1,29 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Logo -->
+{% if config.theme.logo %}
+ <img src="{{ config.theme.logo | url }}" alt="logo" />
+{% else %}
+ {% set icon = config.theme.icon.logo or "material/library" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+{% endif %}
diff --git a/src/templates/partials/nav-item.html b/src/templates/partials/nav-item.html
new file mode 100644
index 00000000..24d74a1a
--- /dev/null
+++ b/src/templates/partials/nav-item.html
@@ -0,0 +1,249 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Render navigation link status -->
+{% macro render_status(nav_item, type) %}
+ {% set class = "md-status md-status--" ~ type %}
+
+ <!-- Render icon with title (or tooltip), if given -->
+ {% if config.extra.status and config.extra.status[type] %}
+ <span
+ class="{{ class }}"
+ title="{{ config.extra.status[type] }}"
+ >
+ </span>
+
+ <!-- Render icon only -->
+ {% else %}
+ <span class="{{ class }}"></span>
+ {% endif %}
+{% endmacro %}
+
+<!-- Render navigation link content -->
+{% macro render_content(nav_item, ref = nav_item) %}
+
+ <!-- Navigation link icon -->
+ {% if nav_item.is_page and nav_item.meta.icon %}
+ {% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %}
+ {% endif %}
+
+ <!-- Navigation link title -->
+ <span class="md-ellipsis">
+ {{ ref.title }}
+ </span>
+
+ <!-- Navigation link status -->
+ {% if nav_item.is_page and nav_item.meta.status %}
+ {{ render_status(nav_item, nav_item.meta.status) }}
+ {% endif %}
+{% endmacro %}
+
+<!-- Render navigation item (pruned) -->
+{% macro render_pruned(nav_item, ref = nav_item) %}
+ {% set first = nav_item.children | first %}
+
+ <!-- Recurse, if the first item has further nested items -->
+ {% if first and first.children %}
+ {{ render_pruned(first, ref) }}
+
+ <!-- Navigation link -->
+ {% else %}
+ <a href="{{ first.url | url }}" class="md-nav__link">
+ {{ render_content(ref) }}
+
+ <!-- Only render toggle if there's at least one nested item -->
+ {% if nav_item.children | length > 0 %}
+ <span class="md-nav__icon md-icon"></span>
+ {% endif %}
+ </a>
+ {% endif %}
+{% endmacro %}
+
+<!-- Render navigation item -->
+{% macro render(nav_item, path, level) %}
+
+ <!-- Determine classes -->
+ {% set class = "md-nav__item" %}
+ {% if nav_item.active %}
+ {% set class = class ~ " md-nav__item--active" %}
+ {% endif %}
+
+ <!-- Navigation item with nested items -->
+ {% if nav_item.children %}
+
+ <!-- Determine all nested items that are index pages -->
+ {% set indexes = [] %}
+ {% if "navigation.indexes" in features %}
+ {% for nav_item in nav_item.children %}
+ {% if nav_item.is_index and not index is defined %}
+ {% set _ = indexes.append(nav_item) %}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+
+ <!-- Determine whether to render item as a section -->
+ {% set tabs = "navigation.tabs" in features %}
+ {% set sections = "navigation.sections" in features %}
+ {% if tabs and level == 1 or sections and tabs >= level - 1 %}
+ {% set class = class ~ " md-nav__item--section" %}
+ {% set is_section = true %}
+
+ <!-- Determine whether to prune inactive item -->
+ {% elif not nav_item.active and "navigation.prune" in features %}
+ {% set class = class ~ " md-nav__item--pruned" %}
+ {% set is_pruned = true %}
+ {% endif %}
+
+ <!-- Nested navigation item -->
+ <li class="{{ class }} md-nav__item--nested">
+ {% if not is_pruned %}
+ {% set checked = "checked" if nav_item.active %}
+
+ <!-- Determine checked and indeterminate state -->
+ {% set is_expanded = "navigation.expand" in features %}
+ {% if is_expanded and not checked %}
+ {% set indeterminate = "md-toggle--indeterminate" %}
+ {% endif %}
+
+ <!-- Active checkbox expands items contained within nested section -->
+ <input
+ class="md-nav__toggle md-toggle {{ indeterminate }}"
+ type="checkbox"
+ id="{{ path }}"
+ {{ checked }}
+ />
+
+ <!-- Toggle to expand nested items -->
+ {% if not indexes %}
+ {% set tabindex = "0" if not is_section %}
+ <label
+ class="md-nav__link"
+ for="{{ path }}"
+ id="{{ path }}_label"
+ tabindex="{{ tabindex }}"
+ >
+ {{ render_content(nav_item) }}
+ <span class="md-nav__icon md-icon"></span>
+ </label>
+
+ <!-- Toggle to expand nested items with link to index page -->
+ {% else %}
+ {% set index = indexes | first %}
+ {% set class = "md-nav__link--active" if index == page %}
+ <div class="md-nav__link md-nav__container">
+ <a
+ href="{{ index.url | url }}"
+ class="md-nav__link {{ class }}"
+ >
+ {{ render_content(index, nav_item) }}
+ </a>
+
+ <!-- Only render toggle if there's at least one more page -->
+ {% if nav_item.children | length > 1 %}
+ {% set tabindex = "0" if not is_section %}
+ <label
+ class="md-nav__link {{ class }}"
+ for="{{ path }}"
+ id="{{ path }}_label"
+ tabindex="{{ tabindex }}"
+ >
+ <span class="md-nav__icon md-icon"></span>
+ </label>
+ {% endif %}
+ </div>
+ {% endif %}
+
+ <!-- Nested navigation -->
+ <nav
+ class="md-nav"
+ data-md-level="{{ level }}"
+ aria-labelledby="{{ path }}_label"
+ aria-expanded="{{ nav_item.active | tojson }}"
+ >
+ <label class="md-nav__title" for="{{ path }}">
+ <span class="md-nav__icon md-icon"></span>
+ {{ nav_item.title }}
+ </label>
+ <ul class="md-nav__list" data-md-scrollfix>
+
+ <!-- Nested navigation item -->
+ {% for nav_item in nav_item.children %}
+ {% if not indexes or nav_item != indexes | first %}
+ {{ render(nav_item, path ~ "_" ~ loop.index, level + 1) }}
+ {% endif %}
+ {% endfor %}
+ </ul>
+ </nav>
+
+ <!-- Pruned navigation item -->
+ {% else %}
+ {{ render_pruned(nav_item) }}
+ {% endif %}
+ </li>
+
+ <!-- Currently active page -->
+ {% elif nav_item == page %}
+ <li class="{{ class }}">
+ {% set toc = page.toc %}
+
+ <!-- State toggle -->
+ <input
+ class="md-nav__toggle md-toggle"
+ type="checkbox"
+ id="__toc"
+ />
+
+ <!-- Hack: see partials/toc.html for more information -->
+ {% set first = toc | first %}
+ {% if first and first.level == 1 %}
+ {% set toc = first.children %}
+ {% endif %}
+
+ <!-- Navigation link to table of contents -->
+ {% if toc %}
+ <label class="md-nav__link md-nav__link--active" for="__toc">
+ {{ render_content(nav_item) }}
+ <span class="md-nav__icon md-icon"></span>
+ </label>
+ {% endif %}
+ <a
+ href="{{ nav_item.url | url }}"
+ class="md-nav__link md-nav__link--active"
+ >
+ {{ render_content(nav_item) }}
+ </a>
+
+ <!-- Table of contents -->
+ {% if toc %}
+ {% include "partials/toc.html" %}
+ {% endif %}
+ </li>
+
+ <!-- Navigation item -->
+ {% else %}
+ <li class="{{ class }}">
+ <a href="{{ nav_item.url | url }}" class="md-nav__link">
+ {{ render_content(nav_item) }}
+ </a>
+ </li>
+ {% endif %}
+{% endmacro %}
diff --git a/src/templates/partials/nav.html b/src/templates/partials/nav.html
new file mode 100644
index 00000000..c41fe694
--- /dev/null
+++ b/src/templates/partials/nav.html
@@ -0,0 +1,69 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+{% import "partials/nav-item.html" as item with context %}
+
+<!-- Determine classes -->
+{% set class = "md-nav md-nav--primary" %}
+{% if "navigation.tabs" in features %}
+ {% set class = class ~ " md-nav--lifted" %}
+{% endif %}
+{% if "toc.integrate" in features %}
+ {% set class = class ~ " md-nav--integrated" %}
+{% endif %}
+
+<!-- Navigation -->
+<nav
+ class="{{ class }}"
+ aria-label="{{ lang.t('nav') }}"
+ data-md-level="0"
+>
+
+ <!-- Site title -->
+ <label class="md-nav__title" for="__drawer">
+ <a
+ href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}"
+ title="{{ config.site_name | e }}"
+ class="md-nav__button md-logo"
+ aria-label="{{ config.site_name }}"
+ data-md-component="logo"
+ >
+ {% include "partials/logo.html" %}
+ </a>
+ {{ config.site_name }}
+ </label>
+
+ <!-- Repository information -->
+ {% if config.repo_url %}
+ <div class="md-nav__source">
+ {% include "partials/source.html" %}
+ </div>
+ {% endif %}
+
+ <!-- Navigation list -->
+ <ul class="md-nav__list" data-md-scrollfix>
+ {% for nav_item in nav %}
+ {% set path = "__nav_" ~ loop.index %}
+ {{ item.render(nav_item, path, 1) }}
+ {% endfor %}
+ </ul>
+</nav>
diff --git a/src/templates/partials/pagination.html b/src/templates/partials/pagination.html
new file mode 100644
index 00000000..046ecbe9
--- /dev/null
+++ b/src/templates/partials/pagination.html
@@ -0,0 +1,42 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Pagination icons -->
+{% import ".icons/material/chevron-double-left.svg" as icon_first %}
+{% import ".icons/material/chevron-left.svg" as icon_previous %}
+{% import ".icons/material/chevron-right.svg" as icon_next %}
+{% import ".icons/material/chevron-double-right.svg" as icon_last %}
+
+<!-- Pagination -->
+<nav class="md-pagination">
+ {{
+ pagination({
+ "link_attr": { "class": "md-pagination__link" },
+ "curpage_attr": { "class": "md-pagination__current" },
+ "dotdot_attr": { "class": "md-pagination__dots" },
+ "symbol_first": icon_first,
+ "symbol_previous": icon_previous,
+ "symbol_next": icon_next,
+ "symbol_last": icon_last
+ })
+ }}
+</nav>
diff --git a/src/templates/partials/palette.html b/src/templates/partials/palette.html
new file mode 100644
index 00000000..ccb8db0a
--- /dev/null
+++ b/src/templates/partials/palette.html
@@ -0,0 +1,55 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Color palette toggle -->
+<form class="md-header__option" data-md-component="palette">
+ {% for option in config.theme.palette %}
+ {% set scheme = option.scheme | d("default", true) %}
+ {% set primary = option.primary | d("indigo", true) %}
+ {% set accent = option.accent | d("indigo", true) %}
+ <input
+ class="md-option"
+ data-md-color-media="{{ option.media }}"
+ data-md-color-scheme="{{ scheme | replace(' ', '-') }}"
+ data-md-color-primary="{{ primary | replace(' ', '-') }}"
+ data-md-color-accent="{{ accent | replace(' ', '-') }}"
+ {% if option.toggle %}
+ aria-label="{{ option.toggle.name }}"
+ {% else %}
+ aria-hidden="true"
+ {% endif %}
+ type="radio"
+ name="__palette"
+ id="__palette_{{ loop.index }}"
+ />
+ {% if option.toggle %}
+ <label
+ class="md-header__button md-icon"
+ title="{{ option.toggle.name }}"
+ for="__palette_{{ loop.index0 or loop.length }}"
+ hidden
+ >
+ {% include ".icons/" ~ option.toggle.icon ~ ".svg" %}
+ </label>
+ {% endif %}
+ {% endfor %}
+</form>
diff --git a/src/templates/partials/post.html b/src/templates/partials/post.html
new file mode 100644
index 00000000..c7233051
--- /dev/null
+++ b/src/templates/partials/post.html
@@ -0,0 +1,99 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Post excerpt -->
+<article class="md-post md-post--excerpt">
+ <header class="md-post__header">
+
+ <!-- Post authors -->
+ {% if post.authors %}
+ <nav class="md-post__authors md-typeset">
+ {% for author in post.authors %}
+ <span class="md-author">
+ <img src="{{ author.avatar }}" alt="{{ author.name }}" />
+ </span>
+ {% endfor %}
+ </nav>
+ {% endif %}
+
+ <!-- Post metadata -->
+ <div class="md-post__meta md-meta">
+ <ul class="md-meta__list">
+
+ <!-- Post date -->
+ <li class="md-meta__item">
+ <time datetime="{{ post.config.date.created }}">
+ {{- post.config.date.created | date -}}
+ </time>
+ {#- Collapse whitespace -#}
+ </li>
+
+ <!-- Post categories -->
+ {% if post.categories %}
+ <li class="md-meta__item">
+ {{ lang.t("blog.categories.in") }}
+ {% for category in post.categories %}
+ <a
+ href="{{ category.url | url }}"
+ class="md-meta__link"
+ >
+ {{- category.title -}}
+ </a>
+ {%- if loop.revindex > 1 %}, {% endif -%}
+ {% endfor -%}
+ </li>
+ {% endif %}
+
+ <!-- Post readtime -->
+ {% if post.config.readtime %}
+ {% set time = post.config.readtime %}
+ <li class="md-meta__item">
+ {% if time == 1 %}
+ {{ lang.t("readtime.one") }}
+ {% else %}
+ {{ lang.t("readtime.other") | replace("#", time) }}
+ {% endif %}
+ </li>
+ {% endif %}
+ </ul>
+
+ <!-- Draft marker -->
+ {% if post.config.draft %}
+ <span class="md-draft">
+ {{ lang.t("blog.draft") }}
+ </span>
+ {% endif %}
+ </div>
+ </header>
+
+ <!-- Post content -->
+ <div class="md-post__content md-typeset">
+ {{ post.content }}
+
+ <!-- Continue reading link -->
+ <nav class="md-post__action">
+ <a href="{{ post.url | url }}">
+ {{ lang.t("blog.continue") }}
+ </a>
+ </nav>
+ </div>
+</article>
diff --git a/src/templates/partials/progress.html b/src/templates/partials/progress.html
new file mode 100644
index 00000000..f5d13d10
--- /dev/null
+++ b/src/templates/partials/progress.html
@@ -0,0 +1,24 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Progress indicator -->
+<div class="md-progress" data-md-component="progress" role="progressbar"></div>
diff --git a/src/templates/partials/search.html b/src/templates/partials/search.html
new file mode 100644
index 00000000..1854a7d3
--- /dev/null
+++ b/src/templates/partials/search.html
@@ -0,0 +1,109 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Search interface -->
+<div class="md-search" data-md-component="search" role="dialog">
+ <label class="md-search__overlay" for="__search"></label>
+ <div class="md-search__inner" role="search">
+ <form class="md-search__form" name="search">
+
+ <!-- Search input -->
+ <input
+ type="text"
+ class="md-search__input"
+ name="query"
+ aria-label="{{ lang.t('search.placeholder') }}"
+ placeholder="{{ lang.t('search.placeholder') }}"
+ autocapitalize="off"
+ autocorrect="off"
+ autocomplete="off"
+ spellcheck="false"
+ data-md-component="search-query"
+ required
+ />
+
+ <!-- Button to open search -->
+ <label class="md-search__icon md-icon" for="__search">
+ {% set icon = config.theme.icon.search or "material/magnify" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ {% set icon = config.theme.icon.previous or "material/arrow-left" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </label>
+
+ <!-- Search options -->
+ <nav
+ class="md-search__options"
+ aria-label="{{ lang.t('search') }}"
+ >
+
+ <!-- Button to share search -->
+ {% if "search.share" in features %}
+ <a
+ href="javascript:void(0)"
+ class="md-search__icon md-icon"
+ title="{{ lang.t('search.share') }}"
+ aria-label="{{ lang.t('search.share') }}"
+ data-clipboard
+ data-clipboard-text=""
+ data-md-component="search-share"
+ tabindex="-1"
+ >
+ {% set icon = config.theme.icon.share or "material/share-variant" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </a>
+ {% endif %}
+
+ <!-- Button to reset search -->
+ <button
+ type="reset"
+ class="md-search__icon md-icon"
+ title="{{ lang.t('search.reset') }}"
+ aria-label="{{ lang.t('search.reset') }}"
+ tabindex="-1"
+ >
+ {% set icon = config.theme.icon.close or "material/close" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </button>
+ </nav>
+
+ <!-- Search suggestions -->
+ {% if "search.suggest" in features %}
+ <div
+ class="md-search__suggest"
+ data-md-component="search-suggest"
+ ></div>
+ {% endif %}
+ </form>
+ <div class="md-search__output">
+ <div class="md-search__scrollwrap" data-md-scrollfix>
+
+ <!-- Search results -->
+ <div class="md-search-result" data-md-component="search-result">
+ <div class="md-search-result__meta">
+ {{ lang.t("search.result.initializer") }}
+ </div>
+ <ol class="md-search-result__list" role="presentation"></ol>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/templates/partials/social.html b/src/templates/partials/social.html
new file mode 100644
index 00000000..5d2c4017
--- /dev/null
+++ b/src/templates/partials/social.html
@@ -0,0 +1,48 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Social links -->
+<div class="md-social">
+ {% for social in config.extra.social %}
+
+ <!-- Automatically set rel=me for Mastodon -->
+ {% set rel = "noopener" %}
+ {% if "mastodon" in social.icon %}
+ {% set rel = rel ~ " me" %}
+ {% endif %}
+
+ <!-- Compute title and render link -->
+ {% set title = social.name %}
+ {% if not title and "//" in social.link %}
+ {% set _, url = social.link.split("//") %}
+ {% set title = url.split("/")[0] %}
+ {% endif %}
+ <a
+ href="{{ social.link }}"
+ target="_blank" rel="{{ rel }}"
+ title="{{ title | e }}"
+ class="md-social__link"
+ >
+ {% include ".icons/" ~ social.icon ~ ".svg" %}
+ </a>
+ {% endfor %}
+</div>
diff --git a/src/templates/partials/source-file.html b/src/templates/partials/source-file.html
new file mode 100644
index 00000000..928e35de
--- /dev/null
+++ b/src/templates/partials/source-file.html
@@ -0,0 +1,44 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Source file information -->
+<hr />
+<div class="md-source-file">
+ <small>
+
+ <!-- mkdocs-git-revision-date-localized-plugin -->
+ {% if page.meta.git_revision_date_localized %}
+ {{ lang.t("source.file.date.updated") }}:
+ {{ page.meta.git_revision_date_localized }}
+ {% if page.meta.git_creation_date_localized %}
+ <br />
+ {{ lang.t("source.file.date.created") }}:
+ {{ page.meta.git_creation_date_localized }}
+ {% endif %}
+
+ <!-- mkdocs-git-revision-date-plugin -->
+ {% elif page.meta.revision_date %}
+ {{ lang.t("source.file.date.updated") }}:
+ {{ page.meta.revision_date }}
+ {% endif %}
+ </small>
+</div>
diff --git a/src/templates/partials/source.html b/src/templates/partials/source.html
new file mode 100644
index 00000000..f4aac3e6
--- /dev/null
+++ b/src/templates/partials/source.html
@@ -0,0 +1,37 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Repository information -->
+<a
+ href="{{ config.repo_url }}"
+ title="{{ lang.t('source') }}"
+ class="md-source"
+ data-md-component="source"
+>
+ <div class="md-source__icon md-icon">
+ {% set icon = config.theme.icon.repo or "fontawesome/brands/git-alt" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ </div>
+ <div class="md-source__repository">
+ {{ config.repo_name }}
+ </div>
+</a>
diff --git a/src/templates/partials/tabs-item.html b/src/templates/partials/tabs-item.html
new file mode 100644
index 00000000..7a12a742
--- /dev/null
+++ b/src/templates/partials/tabs-item.html
@@ -0,0 +1,71 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Render navigation link content -->
+{% macro render_content(nav_item, ref = nav_item) %}
+
+ <!-- Navigation link icon -->
+ {% if nav_item == ref or "navigation.indexes" in features %}
+ {% if nav_item.is_index and nav_item.meta.icon %}
+ {% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %}
+ {% endif %}
+ {% endif %}
+
+ <!-- Navigation link title -->
+ {{ ref.title }}
+{% endmacro %}
+
+<!-- Render navigation item -->
+{% macro render(nav_item, ref = nav_item) %}
+
+ <!-- Determine classes -->
+ {% set class = "md-tabs__item" %}
+ {% if ref.active %}
+ {% set class = class ~ " md-tabs__item--active" %}
+ {% endif %}
+
+ <!-- Navigation item with nested items -->
+ {% if nav_item.children %}
+ {% set first = nav_item.children | first %}
+
+ <!-- Recurse, if the first item has further nested items -->
+ {% if first.children %}
+ {{ render(first, ref) }}
+
+ <!-- Nested navigation item -->
+ {% else %}
+ <li class="{{ class }}">
+ <a href="{{ first.url | url }}" class="md-tabs__link">
+ {{ render_content(first, ref) }}
+ </a>
+ </li>
+ {% endif %}
+
+ <!-- Navigation item -->
+ {% else %}
+ <li class="{{ class }}">
+ <a href="{{ nav_item.url | url }}" class="md-tabs__link">
+ {{ render_content(nav_item) }}
+ </a>
+ </li>
+ {% endif %}
+{% endmacro %}
diff --git a/src/templates/partials/tabs.html b/src/templates/partials/tabs.html
new file mode 100644
index 00000000..0ea590cf
--- /dev/null
+++ b/src/templates/partials/tabs.html
@@ -0,0 +1,38 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+{% import "partials/tabs-item.html" as item with context %}
+
+<!-- Navigation tabs -->
+<nav
+ class="md-tabs"
+ aria-label="{{ lang.t('tabs') }}"
+ data-md-component="tabs"
+>
+ <div class="md-grid">
+ <ul class="md-tabs__list">
+ {% for nav_item in nav %}
+ {{ item.render(nav_item) }}
+ {% endfor %}
+ </ul>
+ </div>
+</nav>
diff --git a/src/templates/partials/tags.html b/src/templates/partials/tags.html
new file mode 100644
index 00000000..b3dea295
--- /dev/null
+++ b/src/templates/partials/tags.html
@@ -0,0 +1,52 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Determine whether to show tags -->
+{% if page.meta and page.meta.hide %}
+ {% set hidden = "hidden" if "tags" in page.meta.hide %}
+{% endif %}
+
+<!-- Tags -->
+<nav class="md-tags" {{ hidden }}>
+ {% for tag in tags %}
+ {% set icon = "" %}
+ {% if config.extra.tags %}
+ {% set icon = " md-tag-icon" %}
+ {% if tag.type %}
+ {% set icon = icon ~ " md-tag--" ~ tag.type %}
+ {% endif %}
+ {% endif %}
+
+ <!-- Render tag with link -->
+ {% if tag.url %}
+ <a href="{{ tag.url | url }}" class="md-tag{{ icon }}">
+ {{- tag.name -}}
+ </a>
+
+ <!-- Render tag without link -->
+ {% else %}
+ <span class="md-tag{{ icon }}">
+ {{- tag.name -}}
+ </span>
+ {% endif %}
+ {% endfor %}
+</nav>
diff --git a/src/templates/partials/toc-item.html b/src/templates/partials/toc-item.html
new file mode 100644
index 00000000..1af82c56
--- /dev/null
+++ b/src/templates/partials/toc-item.html
@@ -0,0 +1,39 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Table of contents item -->
+<li class="md-nav__item">
+ <a href="{{ toc_item.url }}" class="md-nav__link">
+ {{ toc_item.title }}
+ </a>
+
+ <!-- Table of contents list -->
+ {% if toc_item.children %}
+ <nav class="md-nav" aria-label="{{ toc_item.title | striptags }}">
+ <ul class="md-nav__list">
+ {% for toc_item in toc_item.children %}
+ {% include "partials/toc-item.html" %}
+ {% endfor %}
+ </ul>
+ </nav>
+ {% endif %}
+</li>
diff --git a/src/templates/partials/toc.html b/src/templates/partials/toc.html
new file mode 100644
index 00000000..cb50b257
--- /dev/null
+++ b/src/templates/partials/toc.html
@@ -0,0 +1,56 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Determine title -->
+{% set title = lang.t("toc") %}
+{% if config.mdx_configs.toc and config.mdx_configs.toc.title %}
+ {% set title = config.mdx_configs.toc.title %}
+{% endif %}
+
+<!-- Table of contents -->
+<nav class="md-nav md-nav--secondary" aria-label="{{ title }}">
+ {% set toc = page.toc %}
+
+ <!--
+ Check whether the content starts with a level 1 headline. If it does, the
+ top-level anchor must be skipped, since it would be redundant to the link
+ to the current page that is located just above the anchor. Therefore we
+ directly continue with the children of the anchor.
+ -->
+ {% set first = toc | first %}
+ {% if first and first.level == 1 %}
+ {% set toc = first.children %}
+ {% endif %}
+
+ <!-- Table of contents title and list -->
+ {% if toc %}
+ <label class="md-nav__title" for="__toc">
+ <span class="md-nav__icon md-icon"></span>
+ {{ title }}
+ </label>
+ <ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
+ {% for toc_item in toc %}
+ {% include "partials/toc-item.html" %}
+ {% endfor %}
+ </ul>
+ {% endif %}
+</nav>
diff --git a/src/templates/partials/top.html b/src/templates/partials/top.html
new file mode 100644
index 00000000..737e6248
--- /dev/null
+++ b/src/templates/partials/top.html
@@ -0,0 +1,28 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!-- Progress indicator -->
+<button type="button" class="md-top md-icon" data-md-component="top" hidden>
+ {% set icon = config.theme.icon.top or "material/arrow-up" %}
+ {% include ".icons/" ~ icon ~ ".svg" %}
+ {{ lang.t("top") }}
+</button>
diff --git a/src/templates/redirect.html b/src/templates/redirect.html
new file mode 100644
index 00000000..80869c4f
--- /dev/null
+++ b/src/templates/redirect.html
@@ -0,0 +1,41 @@
+<!--
+ Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+-->
+
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <title>{{ config.site_name }}</title>
+ <noscript>
+ <meta http-equiv="refresh" content="0;url={{ page.meta.location }}" />
+ </noscript>
+ <script>
+ window.location.replace([
+ "{{ page.meta.location }}",
+ window.location.search,
+ window.location.hash
+ ].join(""))
+ </script>
+ </head>
+ <body></body>
+</html>