5-72

5-72 helps novice musicians master sound engineering by analyzing the FX chains of world-famous artists
Log | Files | Refs

commit 22511eec8760403536861a62ad0375e79a153c3f
parent 1b8ca699801371ee7e2d26c90d0d9bd6dade85a1
Author: Ismail Dalgatov <29144912+ismaildalgatov@users.noreply.github.com>
Date:   Wed, 24 Aug 2022 14:22:02 +0300

Add src

Diffstat:
M.github/workflows/mkdocs-deploy.yml | 1+
Mmaterial/overrides/partials/content.html | 4++--
Mmkdocs.yml | 4++--
Asrc/.icons/logo.afdesign | 0
Asrc/.icons/logo.svg | 6++++++
Asrc/404.html | 28++++++++++++++++++++++++++++
Asrc/__init__.py | 0
Asrc/assets/images/favicon.png | 0
Asrc/assets/javascripts/_/index.ts | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/document/index.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/_/.eslintrc | 5+++++
Asrc/assets/javascripts/browser/element/_/index.ts | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/focus/index.ts | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/index.ts | 27+++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/offset/_/index.ts | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/offset/content/index.ts | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/offset/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/size/_/index.ts | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/size/content/index.ts | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/size/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/browser/element/visibility/index.ts | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/index.ts | 32++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/keyboard/index.ts | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/location/_/index.ts | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/location/hash/index.ts | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/location/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/browser/media/index.ts | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/request/index.ts | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/script/index.ts | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/toggle/index.ts | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/viewport/_/index.ts | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/viewport/at/index.ts | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/viewport/index.ts | 26++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/viewport/offset/index.ts | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/viewport/size/index.ts | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/browser/worker/index.ts | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/bundle.ts | 273+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/_/index.ts | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/announce/index.ts | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/consent/index.ts | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/_/index.ts | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/annotation/_/index.ts | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/annotation/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/annotation/list/index.ts | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/code/_/index.ts | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/code/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/code/mermaid/index.css | 371+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/code/mermaid/index.ts | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/details/index.ts | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/index.ts | 28++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/table/index.ts | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/content/tabs/index.ts | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/dialog/index.ts | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/header/_/index.ts | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/header/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/components/header/title/index.ts | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/index.ts | 36++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/main/index.ts | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/palette/index.ts | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/search/_/index.ts | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/search/highlight/.eslintrc | 5+++++
Asrc/assets/javascripts/components/search/highlight/index.ts | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/search/index.ts | 28++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/search/query/index.ts | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/search/result/index.ts | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/search/share/index.ts | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/search/suggest/index.ts | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/sidebar/index.ts | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/source/_/index.ts | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/source/facts/_/index.ts | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/source/facts/github/index.ts | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/source/facts/gitlab/index.ts | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/source/facts/index.ts | 25+++++++++++++++++++++++++
Asrc/assets/javascripts/components/source/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/components/tabs/index.ts | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/toc/index.ts | 331+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/components/top/index.ts | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/clipboard/index.ts | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/index.ts | 27+++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/instant/.eslintrc | 6++++++
Asrc/assets/javascripts/integrations/instant/index.ts | 320+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/_/.eslintrc | 6++++++
Asrc/assets/javascripts/integrations/search/_/index.ts | 325+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/document/index.ts | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/highlighter/index.ts | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/index.ts | 28++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/options/index.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/query/_/.eslintrc | 5+++++
Asrc/assets/javascripts/integrations/search/query/_/index.ts | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/query/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/query/transform/.eslintrc | 5+++++
Asrc/assets/javascripts/integrations/search/query/transform/index.ts | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/worker/_/index.ts | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/worker/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/worker/main/.eslintrc | 5+++++
Asrc/assets/javascripts/integrations/search/worker/main/index.ts | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/search/worker/message/index.ts | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/sitemap/index.ts | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/integrations/version/.eslintrc | 5+++++
Asrc/assets/javascripts/integrations/version/index.ts | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/patches/indeterminate/index.ts | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/patches/index.ts | 25+++++++++++++++++++++++++
Asrc/assets/javascripts/patches/scrollfix/index.ts | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/patches/scrolllock/index.ts | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/polyfills/index.ts | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/templates/annotation/index.tsx | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/templates/clipboard/index.tsx | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/templates/index.ts | 29+++++++++++++++++++++++++++++
Asrc/assets/javascripts/templates/search/index.tsx | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/templates/source/index.tsx | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/templates/tabbed/index.tsx | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/templates/table/index.tsx | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/templates/version/index.tsx | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/utilities/h/.eslintrc | 7+++++++
Asrc/assets/javascripts/utilities/h/index.ts | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/utilities/index.ts | 24++++++++++++++++++++++++
Asrc/assets/javascripts/utilities/string/index.ts | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/javascripts/workers/search.ts | 23+++++++++++++++++++++++
Asrc/assets/stylesheets/_config.scss | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main.scss | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/_colors.scss | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/_icons.scss | 37+++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/_modifiers.scss | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/_resets.scss | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/_typeset.scss | 619+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/markdown/_admonition.scss | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/markdown/_footnotes.scss | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/markdown/_toc.scss | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/pymdownx/_critic.scss | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/pymdownx/_details.scss | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/pymdownx/_emoji.scss | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/pymdownx/_highlight.scss | 381+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/pymdownx/_keys.scss | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss | 393+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/integrations/_mermaid.scss | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_banner.scss | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_base.scss | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_clipboard.scss | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_consent.scss | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_content.scss | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_dialog.scss | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_feedback.scss | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_footer.scss | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_form.scss | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_header.scss | 262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_nav.scss | 642+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_search.scss | 713+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_select.scss | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_sidebar.scss | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_source.scss | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_tabs.scss | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_tag.scss | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_tooltip.scss | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_top.scss | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/main/layout/_version.scss | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/palette.scss | 40++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/palette/_accent.scss | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/palette/_primary.scss | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/palette/_scheme.scss | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/utilities/_break.scss | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/stylesheets/utilities/_convert.scss | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/base.html | 412+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main.html | 23+++++++++++++++++++++++
Asrc/mkdocs_theme.yml | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/bundle.ts | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/components/_/index.ts | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/components/iconsearch/_/index.ts | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/components/iconsearch/index.ts | 25+++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/components/iconsearch/query/index.ts | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/components/iconsearch/result/index.ts | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/components/index.ts | 25+++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/components/sponsorship/index.ts | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/integrations/analytics/index.ts | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/integrations/index.ts | 23+++++++++++++++++++++++
Asrc/overrides/assets/javascripts/templates/iconsearch/index.tsx | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/templates/index.ts | 24++++++++++++++++++++++++
Asrc/overrides/assets/javascripts/templates/sponsorship/index.tsx | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/stylesheets/main.scss | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/stylesheets/main/_shame.scss | 25+++++++++++++++++++++++++
Asrc/overrides/assets/stylesheets/main/_typeset.scss | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/stylesheets/main/layout/_banner.scss | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/stylesheets/main/layout/_hero.scss | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/stylesheets/main/layout/_iconsearch.scss | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/assets/stylesheets/main/layout/_sponsorship.scss | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/blog.html | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/home.html | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/main.html | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/overrides/partials/content.html | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/consent.html | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/content.html | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/copyright.html | 33+++++++++++++++++++++++++++++++++
Asrc/partials/feedback.html | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/footer.html | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/header.html | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/icons.html | 37+++++++++++++++++++++++++++++++++++++
Asrc/partials/integrations/analytics.html | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/integrations/analytics/google.html | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/javascripts/announce.html | 31+++++++++++++++++++++++++++++++
Asrc/partials/javascripts/base.html | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/javascripts/consent.html | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/javascripts/content.html | 39+++++++++++++++++++++++++++++++++++++++
Asrc/partials/javascripts/outdated.html | 29+++++++++++++++++++++++++++++
Asrc/partials/javascripts/palette.html | 29+++++++++++++++++++++++++++++
Asrc/partials/language.html | 28++++++++++++++++++++++++++++
Asrc/partials/languages/af.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/ar.html | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/bg.html | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/bn.html | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/ca.html | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/cs.html | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/da.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/de.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/el.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/en.html | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/eo.html | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/es.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/et.html | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/fa.html | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/fi.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/fr.html | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/gl.html | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/he.html | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/hi.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/hr.html | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/hu.html | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/hy.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/id.html | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/is.html | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/it.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/ja.html | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/ka.html | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/kr.html | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/lt.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/lv.html | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/mk.html | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/mn.html | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/ms.html | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/my.html | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/nl.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/nn.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/no.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/pl.html | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/pt-BR.html | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/pt.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/ro.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/ru.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/sh.html | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/si.html | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/sk.html | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/sl.html | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/sr.html | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/sv.html | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/th.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/tl.html | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/tr.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/uk.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/ur.html | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/uz.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/vi.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/zh-Hant.html | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/zh-TW.html | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/languages/zh.html | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/logo.html | 29+++++++++++++++++++++++++++++
Asrc/partials/nav-item.html | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/nav.html | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/palette.html | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/search.html | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/social.html | 40++++++++++++++++++++++++++++++++++++++++
Asrc/partials/source-file.html | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/source.html | 37+++++++++++++++++++++++++++++++++++++
Asrc/partials/tabs-item.html | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/partials/tabs.html | 39+++++++++++++++++++++++++++++++++++++++
Asrc/partials/tags.html | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/partials/toc-item.html | 39+++++++++++++++++++++++++++++++++++++++
Asrc/partials/toc.html | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/__init__.py | 0
Asrc/plugins/search/__init__.py | 0
Asrc/plugins/search/plugin.py | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/tags/__init__.py | 0
Asrc/plugins/tags/plugin.py | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
282 files changed, 25603 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/mkdocs-deploy.yml b/.github/workflows/mkdocs-deploy.yml @@ -17,6 +17,7 @@ jobs: run: | pip install -r requirements.txt pip install wheel + pip install mkdocs-redirects pip install mkdocs-minify-plugin pip install mkdocs-git-revision-date-localized-plugin mkdocs build diff --git a/material/overrides/partials/content.html b/material/overrides/partials/content.html @@ -2,8 +2,8 @@ This file was automatically generated - do not edit -#} {% if page.edit_url %} - {% set edit = "https://github.com/squidfunk/mkdocs-material/edit" %} - {% set view = "https://raw.githubusercontent.com/squidfunk/mkdocs-material" %} + {% set edit = "https://github.com/ismaildalgatov/5-72/edit" %} + {% set view = "https://raw.githubusercontent.com/ismaildalgatov/5-72" %} <a href="{{ page.edit_url }}" title="{{ lang.t('edit.link.title') }}" class="md-content__button md-icon"> {% include ".icons/material/file-edit-outline.svg" %} </a> diff --git a/mkdocs.yml b/mkdocs.yml @@ -79,8 +79,8 @@ plugins: # reference/variables.md: https://mkdocs-macros-plugin.readthedocs.io/ # sponsorship.md: insiders/index.md # upgrading.md: upgrade.md -# - minify: -# minify_html: true + - minify: + minify_html: true # Customization extra: diff --git a/src/.icons/logo.afdesign b/src/.icons/logo.afdesign Binary files differ. diff --git a/src/.icons/logo.svg b/src/.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/404.html b/src/404.html @@ -0,0 +1,28 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/__init__.py b/src/__init__.py diff --git a/src/assets/images/favicon.png b/src/assets/images/favicon.png Binary files differ. diff --git a/src/assets/javascripts/_/index.ts b/src/assets/javascripts/_/index.ts @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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.tabs.link" /* Link content tabs */ + | "header.autohide" /* Hide header */ + | "navigation.expand" /* Automatic expansion */ + | "navigation.indexes" /* Section pages */ + | "navigation.instant" /* Instant loading */ + | "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.integrate" /* Integrated table of contents */ + +/* ------------------------------------------------------------------------- */ + +/** + * Translation + */ +export type Translation = + | "clipboard.copy" /* Copy to clipboard */ + | "clipboard.copied" /* Copied to clipboard */ + | "search.config.lang" /* Search language */ + | "search.config.pipeline" /* Search pipeline */ + | "search.config.separator" /* Search separator */ + | "search.placeholder" /* 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.version.title" /* Version selector */ + +/** + * Translations + */ +export type Translations = Record<Translation, string> + +/* ------------------------------------------------------------------------- */ + +/** + * Versioning + */ +export interface Versioning { + provider: "mike" /* Version provider */ + default?: string /* Default version */ +} + +/** + * Configuration + */ +export interface Config { + base: string /* Base URL */ + features: Flag[] /* Feature flags */ + translations: Translations /* Translations */ + search: string /* Search worker URL */ + 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/assets/javascripts/browser/document/index.ts b/src/assets/javascripts/browser/document/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/element/_/.eslintrc b/src/assets/javascripts/browser/element/_/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "jsdoc/require-jsdoc": "off" + } +} diff --git a/src/assets/javascripts/browser/element/_/index.ts b/src/assets/javascripts/browser/element/_/index.ts @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/element/focus/index.ts b/src/assets/javascripts/browser/element/focus/index.ts @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + startWith +} from "rxjs" + +import { getActiveElement } from "../_" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch element focus + * + * Previously, this function 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. + * + * @param el - Element + * + * @returns Element focus observable + */ +export function watchElementFocus( + el: HTMLElement +): Observable<boolean> { + return merge( + fromEvent(document.body, "focusin"), + fromEvent(document.body, "focusout") + ) + .pipe( + debounceTime(1), + map(() => { + const active = getActiveElement() + return typeof active !== "undefined" + ? el.contains(active) + : false + }), + startWith(el === getActiveElement()), + distinctUntilChanged() + ) +} diff --git a/src/assets/javascripts/browser/element/index.ts b/src/assets/javascripts/browser/element/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/element/offset/_/index.ts b/src/assets/javascripts/browser/element/offset/_/index.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/element/offset/content/index.ts b/src/assets/javascripts/browser/element/offset/content/index.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/element/offset/index.ts b/src/assets/javascripts/browser/element/offset/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/element/size/_/index.ts b/src/assets/javascripts/browser/element/size/_/index.ts @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 ResizeObserver from "resize-observer-polyfill" +import { + NEVER, + Observable, + Subject, + defer, + filter, + finalize, + map, + merge, + of, + shareReplay, + startWith, + switchMap, + tap +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * 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. + * + * @see https://bit.ly/3iIYfEm - Google Groups on performance + */ +const observer$ = defer(() => of( + new ResizeObserver(entries => { + for (const entry of entries) + entry$.next(entry) + }) +)) + .pipe( + 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/assets/javascripts/browser/element/size/content/index.ts b/src/assets/javascripts/browser/element/size/content/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 + } +} diff --git a/src/assets/javascripts/browser/element/size/index.ts b/src/assets/javascripts/browser/element/size/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/element/visibility/index.ts b/src/assets/javascripts/browser/element/visibility/index.ts @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/index.ts b/src/assets/javascripts/browser/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/keyboard/index.ts b/src/assets/javascripts/browser/keyboard/index.ts @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + share +} 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 keyboard + * + * @returns Keyboard observable + */ +export function watchKeyboard(): Observable<Keyboard> { + return 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() + ) +} diff --git a/src/assets/javascripts/browser/location/_/index.ts b/src/assets/javascripts/browser/location/_/index.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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" + +/* ---------------------------------------------------------------------------- + * 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 + * + * @param url - URL to change to + */ +export function setLocation(url: URL): void { + location.href = url.href +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch location + * + * @returns Location subject + */ +export function watchLocation(): Subject<URL> { + return new Subject<URL>() +} diff --git a/src/assets/javascripts/browser/location/hash/index.ts b/src/assets/javascripts/browser/location/hash/index.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + 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.substring(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 + * + * @returns Location hash observable + */ +export function watchLocationHash(): Observable<string> { + return fromEvent<HashChangeEvent>(window, "hashchange") + .pipe( + map(getLocationHash), + startWith(getLocationHash()), + filter(hash => hash.length > 0), + shareReplay(1) + ) +} + +/** + * Watch location target + * + * @returns Location target observable + */ +export function watchLocationTarget(): Observable<HTMLElement> { + return watchLocationHash() + .pipe( + map(id => getOptionalElement(`[id="${id}"]`)!), + filter(el => typeof el !== "undefined") + ) +} diff --git a/src/assets/javascripts/browser/location/index.ts b/src/assets/javascripts/browser/location/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/media/index.ts b/src/assets/javascripts/browser/media/index.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/request/index.ts b/src/assets/javascripts/browser/request/index.ts @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + from, + map, + of, + shareReplay, + switchMap, + throwError +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * 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: RequestInit = { credentials: "same-origin" } +): Observable<Response> { + return from(fetch(`${url}`, options)) + .pipe( + catchError(() => EMPTY), + switchMap(res => res.status !== 200 + ? throwError(() => new Error(res.statusText)) + : of(res) + ) + ) +} + +/** + * 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?: RequestInit +): Observable<T> { + return request(url, options) + .pipe( + switchMap(res => res.json()), + 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?: RequestInit +): 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/assets/javascripts/browser/script/index.ts b/src/assets/javascripts/browser/script/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 didn'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/assets/javascripts/browser/toggle/index.ts b/src/assets/javascripts/browser/toggle/index.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/viewport/_/index.ts b/src/assets/javascripts/browser/viewport/_/index.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/viewport/at/index.ts b/src/assets/javascripts/browser/viewport/at/index.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/viewport/index.ts b/src/assets/javascripts/browser/viewport/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/viewport/offset/index.ts b/src/assets/javascripts/browser/viewport/offset/index.ts @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/viewport/size/index.ts b/src/assets/javascripts/browser/viewport/size/index.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/browser/worker/index.ts b/src/assets/javascripts/browser/worker/index.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + fromEvent, + map, + share, + switchMap, + tap, + throttle +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Worker message + */ +export interface WorkerMessage { + type: unknown /* Message type */ + data?: unknown /* Message data */ +} + +/** + * Worker handler + * + * @template T - Message type + */ +export interface WorkerHandler< + T extends WorkerMessage +> { + tx$: Subject<T> /* Message transmission subject */ + rx$: Observable<T> /* Message receive observable */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + * + * @template T - Worker message type + */ +interface WatchOptions<T extends WorkerMessage> { + tx$: Observable<T> /* Message transmission observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch a web worker + * + * This function returns an observable that sends all values emitted by the + * message observable to the web worker. Web worker communication is expected + * to be bidirectional (request-response) and synchronous. Messages that are + * emitted during a pending request are throttled, the last one is emitted. + * + * @param worker - Web worker + * @param options - Options + * + * @returns Worker message observable + */ +export function watchWorker<T extends WorkerMessage>( + worker: Worker, { tx$ }: WatchOptions<T> +): Observable<T> { + + /* Intercept messages from worker-like objects */ + const rx$ = fromEvent<MessageEvent>(worker, "message") + .pipe( + map(({ data }) => data as T) + ) + + /* Send and receive messages, return hot observable */ + return tx$ + .pipe( + throttle(() => rx$, { leading: true, trailing: true }), + tap(message => worker.postMessage(message)), + switchMap(() => rx$), + share() + ) +} diff --git a/src/assets/javascripts/bundle.ts b/src/assets/javascripts/bundle.ts @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 "array-flat-polyfill" +import "focus-visible" +import "unfetch/polyfill" +import "url-polyfill" + +import { + EMPTY, + NEVER, + Subject, + defer, + delay, + filter, + map, + merge, + mergeWith, + shareReplay, + switchMap +} from "rxjs" + +import { configuration, feature } from "./_" +import { + at, + getOptionalElement, + requestJSON, + setToggle, + watchDocument, + watchKeyboard, + watchLocation, + watchLocationTarget, + watchMedia, + watchPrint, + watchViewport +} from "./browser" +import { + getComponentElement, + getComponentElements, + mountAnnounce, + mountBackToTop, + mountConsent, + mountContent, + mountDialog, + mountHeader, + mountHeaderTitle, + mountPalette, + mountSearch, + mountSearchHiglight, + mountSidebar, + mountSource, + mountTableOfContents, + mountTabs, + watchHeader, + watchMain +} from "./components" +import { + SearchIndex, + setupClipboardJS, + setupInstantLoading, + setupVersionSelector +} from "./integrations" +import { + patchIndeterminate, + patchScrollfix, + patchScrolllock +} from "./patches" +import "./polyfills" + +/* ---------------------------------------------------------------------------- + * 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() +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") + ? __search?.index || requestJSON<SearchIndex>( + new URL("search/search_index.json", config.base) + ) + : NEVER + +/* Set up Clipboard.js integration */ +const alert$ = new Subject<string>() +setupClipboardJS({ alert$ }) + +/* Set up instant loading, if enabled */ +if (feature("navigation.instant")) + setupInstantLoading({ document$, location$, viewport$ }) + +/* 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("[href][rel=prev]") + if (typeof prev !== "undefined") + prev.click() + break + + /* Go to next page */ + case "n": + case ".": + const next = getOptionalElement("[href][rel=next]") + if (typeof next !== "undefined") + next.click() + break + } + }) + +/* 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)), + + /* 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, { 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$, 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.component$ = component$ /* Component observable */ diff --git a/src/assets/javascripts/components/_/index.ts b/src/assets/javascripts/components/_/index.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 */ + | "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 */ + "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/assets/javascripts/components/announce/index.ts b/src/assets/javascripts/components/announce/index.ts @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + startWith, + 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 + + /* Mount component on subscription */ + return defer(() => { + const push$ = new Subject<Announce>() + push$ + .pipe( + startWith({ hash: __md_get<number>("__announce") }) + ) + .subscribe(({ hash }) => { + if (hash && hash === (__md_get<number>("__announce") ?? 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/assets/javascripts/components/consent/index.ts b/src/assets/javascripts/components/consent/index.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 */ +} + +/* ---------------------------------------------------------------------------- + * 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/assets/javascripts/components/content/_/index.ts b/src/assets/javascripts/components/content/_/index.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { getElements } from "~/browser" + +import { Component } from "../../_" +import { Annotation } from "../annotation" +import { + CodeBlock, + Mermaid, + mountCodeBlock, + mountMermaid +} from "../code" +import { + Details, + mountDetails +} from "../details" +import { + DataTable, + mountDataTable +} from "../table" +import { + ContentTabs, + mountContentTabs +} from "../tabs" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Content + */ +export type Content = + | Annotation + | ContentTabs + | CodeBlock + | Mermaid + | DataTable + | Details + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + 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, { target$, print$ }: MountOptions +): Observable<Component<Content>> { + return merge( + + /* Code blocks */ + ...getElements("pre:not(.mermaid) > code", el) + .map(child => mountCodeBlock(child, { 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)) + ) +} diff --git a/src/assets/javascripts/components/content/annotation/_/index.ts b/src/assets/javascripts/components/content/annotation/_/index.ts @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + animationFrameScheduler, + combineLatest, + defer, + finalize, + fromEvent, + map, + switchMap, + take, + takeLast, + takeUntil, + tap, + throttleTime +} from "rxjs" + +import { + ElementOffset, + getElement, + getElementSize, + watchElementContentOffset, + watchElementFocus, + watchElementOffset, + watchElementVisibility +} from "~/browser" + +import { Component } from "../../../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Annotation + */ +export interface Annotation { + active: boolean /* Annotation is active */ + offset: ElementOffset /* Annotation offset */ +} + +/* ---------------------------------------------------------------------------- + * 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]) => { + const { width } = getElementSize(el) + return ({ + x: x - scroll.x + width / 2, + y: y - scroll.y + }) + }) + ) + + /* 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 + * + * @returns Annotation component observable + */ +export function mountAnnotation( + el: HTMLElement, container: HTMLElement +): Observable<Component<Annotation>> { + return defer(() => { + const push$ = new Subject<Annotation>() + 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 */ + const done$ = push$.pipe(takeLast(1)) + watchElementVisibility(el) + .pipe( + takeUntil(done$) + ) + .subscribe(visible => { + el.toggleAttribute("data-md-visible", visible) + }) + + /* Track relative origin of tooltip */ + push$ + .pipe( + throttleTime(500, animationFrameScheduler), + map(() => container.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") + } + }) + + /* Close open annotation on click */ + const index = getElement(":scope > :last-child", el) + const blur$ = fromEvent(index, "mousedown", { once: true }) + push$ + .pipe( + switchMap(({ active }) => active ? blur$ : EMPTY), + tap(ev => ev.preventDefault()) + ) + .subscribe(() => el.blur()) + + /* 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/assets/javascripts/components/content/annotation/index.ts b/src/assets/javascripts/components/content/annotation/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 "./list" diff --git a/src/assets/javascripts/components/content/annotation/list/index.ts b/src/assets/javascripts/components/content/annotation/list/index.ts @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + merge, + share, + takeLast, + 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 { + print$: Observable<boolean> /* Media print observable */ +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Find all annotation markers in the given code block + * + * @param container - Containing element + * + * @returns Annotation markers + */ +function findAnnotationMarkers(container: HTMLElement): Text[] { + const markers: Text[] = [] + for (const comment of getElements(".c, .c1, .cm", container)) { + let match: RegExpExecArray | null + + /* Split text at marker and add to list */ + let text = comment.firstChild as Text + if (text instanceof Text) + while ((match = /\((\d+)\)/.exec(text.textContent!))) { + const marker = text.splitText(match.index) + text = marker.splitText(match[0].length) + markers.push(marker) + } + } + 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, { print$ }: MountOptions +): Observable<Component<Annotation>> { + + /* Find and replace all markers with empty annotations */ + const annotations = new Map<number, HTMLElement>() + for (const marker of findAnnotationMarkers(container)) { + const [, id] = marker.textContent!.match(/\((\d+)\)/)! + if (getOptionalElement(`li:nth-child(${id})`, el)) { + annotations.set(+id, renderAnnotation(+id)) + marker.replaceWith(annotations.get(+id)!) + } + } + + /* Keep list if there are no annotations to render */ + if (annotations.size === 0) + return EMPTY + + /* Create and return component */ + return defer(() => { + const done$ = new Subject<void>() + + /* Handle print mode - see https://bit.ly/3rgPdpt */ + print$ + .pipe( + takeUntil(done$.pipe(takeLast(1))) + ) + .subscribe(active => { + el.hidden = !active + + /* Show annotations in code block or list (print) */ + for (const [id, annotation] of annotations) { + const inner = getElement(".md-typeset", annotation) + const child = getElement(`li:nth-child(${id})`, el) + if (!active) + swap(child, inner) + else + swap(inner, child) + } + }) + + /* Create and return component */ + return merge(...[...annotations] + .map(([, annotation]) => ( + mountAnnotation(annotation, container) + )) + ) + .pipe( + finalize(() => done$.complete()), + share() + ) + }) +} diff --git a/src/assets/javascripts/components/content/code/_/index.ts b/src/assets/javascripts/components/content/code/_/index.ts @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + takeLast, + takeUntil, + 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 { + print$: Observable<boolean> /* Media print observable */ +} + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Global sequence number for Clipboard.js integration + */ +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. + * + * @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()) { + 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( + takeUntil(push$.pipe(takeLast(1))), + 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 on first sight */ + return watchElementVisibility(el) + .pipe( + filter(visible => visible), + take(1), + switchMap(() => factory$) + ) +} diff --git a/src/assets/javascripts/components/content/code/index.ts b/src/assets/javascripts/components/content/code/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 "./mermaid" diff --git a/src/assets/javascripts/components/content/code/mermaid/index.css b/src/assets/javascripts/components/content/code/mermaid/index.css @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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); +} + +/* 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 */ +defs #flowchart-circleStart, +defs #flowchart-circleEnd, +defs #flowchart-crossStart, +defs #flowchart-crossEnd, +defs #flowchart-pointStart, +defs #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 + * ------------------------------------------------------------------------- */ + + /* 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-label-bg-color); + stroke: var(--md-mermaid-node-fg-color); +} + +/* Sequence actor text */ +text.actor > tspan { + font-family: var(--md-mermaid-font-family); + fill: var(--md-mermaid-label-fg-color); +} + +/* Sequence actor line */ +line { + stroke: var(--md-default-fg-color--lighter); +} + +/* Sequence message line */ +.messageLine0, +.messageLine1 { + stroke: var(--md-mermaid-edge-color); +} + +/* Sequence message and loop text */ +.messageText, +.loopText > tspan { + font-family: var(--md-mermaid-font-family) !important; + fill: var(--md-mermaid-edge-color); + stroke: none; +} + +/* Sequence arrow head */ +#arrowhead path { + fill: var(--md-mermaid-edge-color); + stroke: none; +} + +/* Sequence loop line */ +.loopLine { + fill: var(--md-mermaid-node-bg-color); + stroke: var(--md-mermaid-node-fg-color); +} + +/* Sequence label box */ +.labelBox { + fill: var(--md-mermaid-node-bg-color); + stroke: none; +} + +/* Sequence label text */ +.labelText, +.labelText > span { + font-family: var(--md-mermaid-font-family); + fill: var(--md-mermaid-node-fg-color); +} diff --git a/src/assets/javascripts/components/content/code/mermaid/index.ts b/src/assets/javascripts/components/content/code/mermaid/index.ts @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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.0.1/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 + })), + map(() => undefined), + shareReplay(1) + ) + + /* Render diagram */ + mermaid$.subscribe(() => { + el.classList.add("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du + const id = `__mermaid_${sequence++}` + const host = h("div", { class: "mermaid" }) + mermaid.mermaidAPI.render(id, el.textContent, (svg: string) => { + + /* Create a shadow root and inject diagram */ + const shadow = host.attachShadow({ mode: "closed" }) + shadow.innerHTML = svg + + /* Replace code block with diagram */ + el.replaceWith(host) + }) + }) + + /* Create and return component */ + return mermaid$ + .pipe( + map(() => ({ ref: el })) + ) +} diff --git a/src/assets/javascripts/components/content/details/index.ts b/src/assets/javascripts/components/content/details/index.ts @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 }) => { + if (action === "open") + el.setAttribute("open", "") + else + el.removeAttribute("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/assets/javascripts/components/content/index.ts b/src/assets/javascripts/components/content/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/content/table/index.ts b/src/assets/javascripts/components/content/table/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/content/tabs/index.ts b/src/assets/javascripts/components/content/tabs/index.ts @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + finalize, + fromEvent, + map, + merge, + skip, + startWith, + subscribeOn, + takeLast, + takeUntil, + tap +} from "rxjs" + +import { feature } from "~/_" +import { + 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 */ +} + +/* ---------------------------------------------------------------------------- + * 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 + * + * @returns Content tabs component observable + */ +export function mountContentTabs( + el: HTMLElement +): 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(takeLast(1)) + 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)) + .subscribe(({ active }) => { + const tab = active.innerText.trim() + 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.innerText.trim() === tab) { + input.click() + break + } + } + + /* Persist active tabs in local storage */ + const tabs = __md_get<string[]>("__tabs") || [] + __md_set("__tabs", [...new Set([tab, ...tabs])]) + }) + + /* 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/assets/javascripts/components/dialog/index.ts b/src/assets/javascripts/components/dialog/index.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/header/_/index.ts b/src/assets/javascripts/components/header/_/index.ts @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + filter, + map, + of, + shareReplay, + startWith, + switchMap, + takeLast, + 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(takeLast(1)) + 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/assets/javascripts/components/header/index.ts b/src/assets/javascripts/components/header/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/header/title/index.ts b/src/assets/javascripts/components/header/title/index.ts @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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(({ active }) => { + el.classList.toggle("md-header__title--active", active) + }) + + /* Obtain headline, if any */ + const heading = getOptionalElement("article 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/assets/javascripts/components/index.ts b/src/assets/javascripts/components/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 "./search" +export * from "./sidebar" +export * from "./source" +export * from "./tabs" +export * from "./toc" +export * from "./top" diff --git a/src/assets/javascripts/components/main/index.ts b/src/assets/javascripts/components/main/index.ts @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/palette/index.ts b/src/assets/javascripts/components/palette/index.ts @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { Component } 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>> { + 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) + }) + + /* 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/assets/javascripts/components/search/_/index.ts b/src/assets/javascripts/components/search/_/index.ts @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + merge, + mergeWith, + sample, + take +} from "rxjs" + +import { configuration } from "~/_" +import { + Keyboard, + getActiveElement, + getElements, + setToggle +} from "~/browser" +import { + SearchIndex, + SearchResult, + isSearchQueryMessage, + isSearchReadyMessage, + 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 url = __search?.worker || config.search + const worker = setupSearchWorker(url, index$) + + /* Retrieve query and result components */ + const query = getComponentElement("search-query", el) + const result = getComponentElement("search-result", el) + + /* Re-emit query when search is ready */ + const { tx$, rx$ } = worker + tx$ + .pipe( + filter(isSearchQueryMessage), + sample(rx$.pipe(filter(isSearchReadyMessage))), + take(1) + ) + .subscribe(tx$.next.bind(tx$)) + + /* 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) + const result$ = mountSearchResult(result, worker, { query$ }) + return merge(query$, result$) + .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/assets/javascripts/components/search/highlight/.eslintrc b/src/assets/javascripts/components/search/highlight/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "no-null/no-null": "off" + } +} diff --git a/src/assets/javascripts/components/search/highlight/index.ts b/src/assets/javascripts/components/search/highlight/index.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, true)( + 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/assets/javascripts/components/search/index.ts b/src/assets/javascripts/components/search/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/search/query/index.ts b/src/assets/javascripts/components/search/query/index.ts @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + delay, + distinctUntilChanged, + distinctUntilKeyChanged, + filter, + finalize, + fromEvent, + map, + merge, + share, + shareReplay, + startWith, + take, + takeLast, + takeUntil, + tap +} from "rxjs" + +import { translation } from "~/_" +import { + getLocation, + setToggle, + watchElementFocus, + watchToggle +} from "~/browser" +import { + SearchMessageType, + SearchQueryMessage, + SearchWorker, + defaultTransform, + isSearchReadyMessage +} from "~/integrations" + +import { Component } from "../../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search query + */ +export interface SearchQuery { + value: string /* Query value */ + focus: boolean /* Query focus */ +} + +/* ---------------------------------------------------------------------------- + * 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 worker - Search worker + * + * @returns Search query observable + */ +export function watchSearchQuery( + el: HTMLInputElement, { rx$ }: SearchWorker +): Observable<SearchQuery> { + const fn = __search?.transform || defaultTransform + + /* Immediately show search dialog */ + const { searchParams } = getLocation() + if (searchParams.has("q")) + setToggle("search", true) + + /* Intercept query parameter (deep link) */ + const param$ = rx$ + .pipe( + filter(isSearchReadyMessage), + take(1), + map(() => searchParams.get("q") || "") + ) + + /* Remove query parameter when search is closed */ + watchToggle("search") + .pipe( + filter(active => !active), + take(1) + ) + .subscribe(() => { + const url = new URL(location.href) + url.searchParams.delete("q") + history.replaceState({}, "", `${url}`) + }) + + /* Set query from parameter */ + param$.subscribe(value => { // TODO: not ideal - find a better way + if (value) { + el.value = value + el.focus() + } + }) + + /* Intercept focus and input events */ + const focus$ = watchElementFocus(el) + const value$ = merge( + fromEvent(el, "keyup"), + fromEvent(el, "focus").pipe(delay(1)), + param$ + ) + .pipe( + map(() => fn(el.value)), + startWith(""), + 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 worker - Search worker + * + * @returns Search query component observable + */ +export function mountSearchQuery( + el: HTMLInputElement, { tx$, rx$ }: SearchWorker +): Observable<Component<SearchQuery, HTMLInputElement>> { + const push$ = new Subject<SearchQuery>() + const done$ = push$.pipe(takeLast(1)) + + /* Handle value changes */ + push$ + .pipe( + distinctUntilKeyChanged("value"), + map(({ value }): SearchQueryMessage => ({ + type: SearchMessageType.QUERY, + data: value + })) + ) + .subscribe(tx$.next.bind(tx$)) + + /* Handle focus changes */ + push$ + .pipe( + distinctUntilKeyChanged("focus") + ) + .subscribe(({ focus }) => { + if (focus) { + setToggle("search", focus) + el.placeholder = "" + } else { + el.placeholder = translation("search.placeholder") + } + }) + + /* Handle reset */ + fromEvent(el.form!, "reset") + .pipe( + takeUntil(done$) + ) + .subscribe(() => el.focus()) + + /* Create and return component */ + return watchSearchQuery(el, { tx$, rx$ }) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })), + share() + ) +} diff --git a/src/assets/javascripts/components/search/result/index.ts b/src/assets/javascripts/components/search/result/index.ts @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + filter, + finalize, + map, + merge, + of, + skipUntil, + switchMap, + take, + tap, + withLatestFrom, + zipWith +} from "rxjs" + +import { translation } from "~/_" +import { + getElement, + watchElementBoundary +} from "~/browser" +import { + SearchResult, + SearchWorker, + 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 */ +} + +/* ---------------------------------------------------------------------------- + * 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 worker - Search worker + * @param options - Options + * + * @returns Search result list component observable + */ +export function mountSearchResult( + el: HTMLElement, { rx$ }: SearchWorker, { query$ }: MountOptions +): Observable<Component<SearchResult>> { + const push$ = new Subject<SearchResult>() + const boundary$ = watchElementBoundary(el.parentElement!) + .pipe( + filter(Boolean) + ) + + /* Retrieve nested components */ + const meta = getElement(":scope > :first-child", el) + const list = getElement(":scope > :last-child", el) + + /* Wait until search is ready */ + const ready$ = rx$ + .pipe( + filter(isSearchReadyMessage), + take(1) + ) + + /* Update search result metadata */ + push$ + .pipe( + withLatestFrom(query$), + skipUntil(ready$) + ) + .subscribe(([{ items }, { value }]) => { + if (value) { + switch (items.length) { + + /* No results */ + case 0: + meta.textContent = translation("search.result.none") + break + + /* One result */ + case 1: + meta.textContent = translation("search.result.one") + break + + /* Multiple result */ + default: + meta.textContent = translation( + "search.result.other", + round(items.length) + ) + } + } else { + meta.textContent = translation("search.result.placeholder") + } + }) + + /* Update search result list */ + 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) + ) + )) + ) + .subscribe(result => list.appendChild( + renderSearchResultItem(result) + )) + + /* Filter search result message */ + const result$ = rx$ + .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/assets/javascripts/components/search/share/index.ts b/src/assets/javascripts/components/search/share/index.ts @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + fromEvent, + map, + 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 = "" + url.searchParams.delete("h") + url.searchParams.set("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>() + push$.subscribe(({ url }) => { + el.setAttribute("data-clipboard-text", el.href) + el.href = `${url}` + }) + + /* Prevent following of link */ + fromEvent(el, "click") + .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/assets/javascripts/components/search/suggest/index.ts b/src/assets/javascripts/components/search/suggest/index.ts @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + SearchResult, + SearchWorker, + isSearchResultMessage +} from "~/integrations" + +import { Component, getComponentElement } from "../../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search suggestions + */ +export interface SearchSuggest {} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + keyboard$: Observable<Keyboard> /* Keyboard observable */ +} + +/* ---------------------------------------------------------------------------- + * 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 worker - Search worker + * @param options - Options + * + * @returns Search result list component observable + */ +export function mountSearchSuggest( + el: HTMLElement, { rx$ }: SearchWorker, { 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(([{ suggestions }, value]) => { + const words = value.split(/([\s-]+)/) + if (suggestions?.length && words[words.length - 1]) { + const last = suggestions[suggestions.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$ = rx$ + .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/assets/javascripts/components/sidebar/index.ts b/src/assets/javascripts/components/sidebar/index.ts @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + defer, + distinctUntilChanged, + finalize, + map, + tap, + withLatestFrom +} from "rxjs" + +import { + Viewport, + getElement, + getElementOffset +} 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.parentElement! + 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>() + push$ + .pipe( + auditTime(0, animationFrameScheduler), + 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 = "" + } + }) + + /* 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/assets/javascripts/components/source/_/index.ts b/src/assets/javascripts/components/source/_/index.ts @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { renderSourceFacts } from "~/templates" + +import { Component } 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 + 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/assets/javascripts/components/source/facts/_/index.ts b/src/assets/javascripts/components/source/facts/_/index.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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> { + const [type] = url.match(/(git(?:hub|lab))/i) || [] + switch (type.toLowerCase()) { + + /* GitHub repository */ + case "github": + const [, user, repo] = url.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i)! + return fetchSourceFactsFromGitHub(user, repo) + + /* GitLab repository */ + case "gitlab": + const [, base, slug] = url.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i)! + return fetchSourceFactsFromGitLab(base, slug) + + /* Everything else */ + default: + return EMPTY + } +} diff --git a/src/assets/javascripts/components/source/facts/github/index.ts b/src/assets/javascripts/components/source/facts/github/index.ts @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/source/facts/gitlab/index.ts b/src/assets/javascripts/components/source/facts/gitlab/index.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/source/facts/index.ts b/src/assets/javascripts/components/source/facts/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/source/index.ts b/src/assets/javascripts/components/source/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/tabs/index.ts b/src/assets/javascripts/components/tabs/index.ts @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/components/toc/index.ts b/src/assets/javascripts/components/toc/index.ts @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + combineLatestWith, + debounceTime, + defer, + distinctUntilChanged, + distinctUntilKeyChanged, + finalize, + map, + of, + repeat, + scan, + share, + skip, + startWith, + switchMap, + takeLast, + takeUntil, + tap, + withLatestFrom +} from "rxjs" + +import { feature } from "~/_" +import { + Viewport, + getElement, + getElements, + getLocation, + getOptionalElement, + watchElementSize +} from "~/browser" + +import { + Component, + getComponentElement +} from "../_" +import { Header } from "../header" + +/* ---------------------------------------------------------------------------- + * 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 */ + 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 + } + + /* 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$, target$ }: MountOptions +): Observable<Component<TableOfContents>> { + return defer(() => { + const push$ = new Subject<TableOfContents>() + const done$ = push$.pipe(takeLast(1)) + 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 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/assets/javascripts/components/top/index.ts b/src/assets/javascripts/components/top/index.ts @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + map, + repeat, + skip, + takeLast, + 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(takeLast(1)) + 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` + }) + + /* 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/assets/javascripts/integrations/clipboard/index.ts b/src/assets/javascripts/integrations/clipboard/index.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 text = 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/assets/javascripts/integrations/index.ts b/src/assets/javascripts/integrations/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/integrations/instant/.eslintrc b/src/assets/javascripts/integrations/instant/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-self-assign": "off", + "no-null/no-null": "off" + } +} diff --git a/src/assets/javascripts/integrations/instant/index.ts b/src/assets/javascripts/integrations/instant/index.ts @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + NEVER, + Observable, + Subject, + bufferCount, + catchError, + concatMap, + debounceTime, + distinctUntilChanged, + distinctUntilKeyChanged, + filter, + fromEvent, + map, + merge, + of, + sample, + share, + skip, + skipUntil, + switchMap +} from "rxjs" + +import { configuration, feature } from "~/_" +import { + Viewport, + ViewportOffset, + getElements, + getOptionalElement, + request, + setLocation, + setLocationHash +} from "~/browser" +import { getComponentElement } from "~/components" +import { h } from "~/utilities" + +import { fetchSitemap } from "../sitemap" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * History state + */ +export interface HistoryState { + url: URL /* State URL */ + offset?: ViewportOffset /* State viewport offset */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Setup options + */ +interface SetupOptions { + document$: Subject<Document> /* Document subject */ + location$: Subject<URL> /* Location subject */ + viewport$: Observable<Viewport> /* Viewport observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Set up instant loading + * + * When fetching, theoretically, 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 could use the old location. + * + * This is the reason why we need to synchronize history events and the process + * of fetching the document for navigation changes (except `popstate` events): + * + * 1. Fetch document via `XMLHTTPRequest` + * 2. Set new location via `history.pushState` + * 3. Parse and emit fetched document + * + * For `popstate` events, we must not use `history.pushState`, or the forward + * history will be irreversibly overwritten. In case the request fails, the + * location change is dispatched regularly. + * + * @param options - Options + */ +export function setupInstantLoading( + { document$, location$, viewport$ }: SetupOptions +): void { + const config = configuration() + if (location.protocol === "file:") + return + + /* Disable automatic scroll restoration */ + if ("scrollRestoration" in history) { + history.scrollRestoration = "manual" + + /* Hack: ensure that reloads restore viewport offset */ + fromEvent(window, "beforeunload") + .subscribe(() => { + history.scrollRestoration = "auto" + }) + } + + /* Hack: ensure absolute favicon link to omit 404s when switching */ + const favicon = getOptionalElement<HTMLLinkElement>("link[rel=icon]") + if (typeof favicon !== "undefined") + favicon.href = favicon.href + + /* Intercept internal navigation */ + const push$ = fetchSitemap() + .pipe( + map(paths => paths.map(path => `${new URL(path, config.base)}`)), + switchMap(urls => fromEvent<MouseEvent>(document.body, "click") + .pipe( + filter(ev => !ev.metaKey && !ev.ctrlKey), + switchMap(ev => { + if (ev.target instanceof Element) { + const el = ev.target.closest("a") + if (el && !el.target) { + const url = new URL(el.href) + + /* Canonicalize URL */ + url.search = "" + url.hash = "" + + /* Check if URL should be intercepted */ + if ( + url.pathname !== location.pathname && + urls.includes(url.toString()) + ) { + ev.preventDefault() + return of({ + url: new URL(el.href) + }) + } + } + } + return NEVER + }) + ) + ), + share<HistoryState>() + ) + + /* Intercept history back and forward */ + const pop$ = fromEvent<PopStateEvent>(window, "popstate") + .pipe( + filter(ev => ev.state !== null), + map(ev => ({ + url: new URL(location.href), + offset: ev.state + })), + share<HistoryState>() + ) + + /* Emit location change */ + merge(push$, pop$) + .pipe( + distinctUntilChanged((a, b) => a.url.href === b.url.href), + map(({ url }) => url) + ) + .subscribe(location$) + + /* Fetch document via `XMLHTTPRequest` */ + const response$ = location$ + .pipe( + distinctUntilKeyChanged("pathname"), + switchMap(url => request(url.href) + .pipe( + catchError(() => { + setLocation(url) + return NEVER + }) + ) + ), + share() + ) + + /* Set new location via `history.pushState` */ + push$ + .pipe( + sample(response$) + ) + .subscribe(({ url }) => { + history.pushState({}, "", `${url}`) + }) + + /* Parse and emit fetched document */ + const dom = new DOMParser() + response$ + .pipe( + switchMap(res => res.text()), + map(res => dom.parseFromString(res, "text/html")) + ) + .subscribe(document$) + + /* Replace meta tags and components */ + document$ + .pipe( + skip(1) + ) + .subscribe(replacement => { + for (const selector of [ + + /* Meta tags */ + "title", + "link[rel=canonical]", + "meta[name=author]", + "meta[name=description]", + + /* Components */ + "[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, replacement) + if ( + typeof source !== "undefined" && + typeof target !== "undefined" + ) { + source.replaceWith(target) + } + } + }) + + /* Re-evaluate scripts */ + document$ + .pipe( + skip(1), + map(() => getComponentElement("container")), + switchMap(el => getElements("script", el)), + concatMap(el => { + const script = h("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 + } + }) + ) + .subscribe() + + /* Emit history state change */ + merge(push$, pop$) + .pipe( + sample(document$) + ) + .subscribe(({ url, offset }) => { + if (url.hash && !offset) { + setLocationHash(url.hash) + } else { + window.scrollTo(0, offset?.y || 0) + } + }) + + /* Debounce update of viewport offset */ + viewport$ + .pipe( + skipUntil(push$), + debounceTime(250), + distinctUntilKeyChanged("offset") + ) + .subscribe(({ offset }) => { + history.replaceState(offset, "") + }) + + /* Set viewport offset from history */ + merge(push$, pop$) + .pipe( + bufferCount(2, 1), + filter(([a, b]) => a.url.pathname === b.url.pathname), + map(([, state]) => state) + ) + .subscribe(({ offset }) => { + window.scrollTo(0, offset?.y || 0) + }) +} diff --git a/src/assets/javascripts/integrations/search/_/.eslintrc b/src/assets/javascripts/integrations/search/_/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "no-console": "off" + } +} diff --git a/src/assets/javascripts/integrations/search/_/index.ts b/src/assets/javascripts/integrations/search/_/index.ts @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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, + SearchDocumentMap, + setupSearchDocumentMap +} from "../document" +import { + SearchHighlightFactoryFn, + setupSearchHighlighter +} from "../highlighter" +import { SearchOptions } from "../options" +import { + SearchQueryTerms, + getSearchQueryTerms, + parseSearchQuery +} from "../query" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search index configuration + */ +export interface SearchIndexConfig { + lang: string[] /* Search languages */ + separator: string /* Search separator */ +} + +/** + * Search index document + */ +export interface SearchIndexDocument { + location: string /* Document location */ + title: string /* Document title */ + text: string /* Document text */ + tags?: string[] /* Document tags */ + boost?: number /* Document boost */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Search index + * + * This interfaces describes the format of the `search_index.json` file which + * is automatically built by the MkDocs search plugin. + */ +export interface SearchIndex { + config: SearchIndexConfig /* Search index configuration */ + docs: SearchIndexDocument[] /* Search index documents */ + options: SearchOptions /* Search options */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Search metadata + */ +export interface SearchMetadata { + score: number /* Score (relevance) */ + terms: SearchQueryTerms /* Search query terms */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Search result document + */ +export type SearchResultDocument = SearchDocument & SearchMetadata + +/** + * Search result item + */ +export type SearchResultItem = SearchResultDocument[] + +/* ------------------------------------------------------------------------- */ + +/** + * Search result + */ +export interface SearchResult { + items: SearchResultItem[] /* Search result items */ + suggestions?: string[] /* Search suggestions */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * 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 mapping + * + * A mapping of URLs (including hash fragments) to the actual articles and + * sections of the documentation. The search document mapping must be created + * regardless of whether the index was prebuilt or not, as Lunr.js itself + * only stores the actual index. + */ + protected documents: SearchDocumentMap + + /** + * Search highlight factory function + */ + protected highlight: SearchHighlightFactoryFn + + /** + * The underlying Lunr.js search index + */ + protected index: lunr.Index + + /** + * Search options + */ + protected options: SearchOptions + + /** + * Create the search integration + * + * @param data - Search index + */ + public constructor({ config, docs, options }: SearchIndex) { + this.options = options + + /* Set up document map and highlighter factory */ + this.documents = setupSearchDocumentMap(docs) + this.highlight = setupSearchHighlighter(config, false) + + /* Set separator for tokenizer */ + lunr.tokenizer.separator = new RegExp(config.separator) + + /* Create search index */ + this.index = lunr(function () { + + /* Set up multi-language support */ + if (config.lang.length === 1 && config.lang[0] !== "en") { + this.use((lunr as any)[config.lang[0]]) + } else if (config.lang.length > 1) { + this.use((lunr as any).multiLanguage(...config.lang)) + } + + /* Compute functions to be removed from the pipeline */ + const fns = difference([ + "trimmer", "stopWordFilter", "stemmer" + ], options.pipeline) + + /* Remove functions from the pipeline for registered languages */ + for (const lang of config.lang.map(language => ( + language === "en" ? lunr : (lunr as any)[language] + ))) { + for (const fn of fns) { + this.pipeline.remove(lang[fn]) + this.searchPipeline.remove(lang[fn]) + } + } + + /* Set up reference */ + this.ref("location") + + /* Set up fields */ + this.field("title", { boost: 1e3 }) + this.field("text") + this.field("tags", { boost: 1e6, extractor: doc => { + const { tags = [] } = doc as SearchDocument + return tags.reduce((list, tag) => [ + ...list, + ...lunr.tokenizer(tag) + ], [] as lunr.Token[]) + } }) + + /* Index documents */ + for (const doc of docs) + this.add(doc, { boost: doc.boost }) + }) + } + + /** + * Search for matching documents + * + * The search index which MkDocs provides is divided up into articles, which + * contain the whole content of the individual pages, and sections, which only + * contain the contents of the subsections obtained by breaking the individual + * pages up at `h1` ... `h6`. As there may be many sections on different pages + * with identical titles (for example within this very project, e.g. "Usage" + * or "Installation"), they need to be put into the context of the containing + * page. For this reason, section results are grouped within their respective + * articles which are the top-level results that are returned. + * + * @param query - Query value + * + * @returns Search results + */ + public search(query: string): SearchResult { + if (query) { + try { + const highlight = this.highlight(query) + + /* 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<SearchResultItem>((item, { ref, score, matchData }) => { + const document = this.documents.get(ref) + if (typeof document !== "undefined") { + const { location, title, text, tags, parent } = document + + /* Compute and analyze search query terms */ + const terms = getSearchQueryTerms( + clauses, + Object.keys(matchData.metadata) + ) + + /* Highlight title and text and apply post-query boosts */ + const boost = +!parent + +Object.values(terms).every(t => t) + item.push({ + location, + title: highlight(title), + text: highlight(text), + ...tags && { tags: tags.map(highlight) }, + score: score * (1 + boost), + terms + }) + } + return item + }, []) + + /* Sort search results again after applying boosts */ + .sort((a, b) => b.score - a.score) + + /* Group search results by page */ + .reduce((items, result) => { + const document = this.documents.get(result.location) + if (typeof document !== "undefined") { + const ref = "parent" in document + ? document.parent!.location + : document.location + items.set(ref, [...items.get(ref) || [], result]) + } + return items + }, new Map<string, SearchResultItem>()) + + /* Generate search suggestions, if desired */ + let suggestions: string[] | undefined + if (this.options.suggestions) { + 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 */ + suggestions = titles.length + ? Object.keys(titles[0].matchData.metadata) + : [] + } + + /* Return items and suggestions */ + return { + items: [...groups.values()], + ...typeof suggestions !== "undefined" && { suggestions } + } + + /* Log errors to console (for now) */ + } catch { + console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`) + } + } + + /* Return nothing in case of error or empty query */ + return { items: [] } + } +} diff --git a/src/assets/javascripts/integrations/search/document/index.ts b/src/assets/javascripts/integrations/search/document/index.ts @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { SearchIndexDocument } from "../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search document + */ +export interface SearchDocument extends SearchIndexDocument { + parent?: SearchIndexDocument /* Parent article */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Search document mapping + */ +export type SearchDocumentMap = Map<string, SearchDocument> + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create a search document mapping + * + * @param docs - Search index documents + * + * @returns Search document map + */ +export function setupSearchDocumentMap( + docs: SearchIndexDocument[] +): SearchDocumentMap { + const documents = new Map<string, SearchDocument>() + const parents = new Set<SearchDocument>() + for (const doc of docs) { + const [path, hash] = doc.location.split("#") + + /* Extract location, title and tags */ + const location = doc.location + const title = doc.title + const tags = doc.tags + + /* Escape and cleanup text */ + const text = escapeHTML(doc.text) + .replace(/\s+(?=[,.:;!?])/g, "") + .replace(/\s+/g, " ") + + /* Handle section */ + if (hash) { + const parent = documents.get(path)! + + /* Ignore first section, override article */ + if (!parents.has(parent)) { + parent.title = doc.title + parent.text = text + + /* Remember that we processed the article */ + parents.add(parent) + + /* Add subsequent section */ + } else { + documents.set(location, { + location, + title, + text, + parent + }) + } + + /* Add article */ + } else { + documents.set(location, { + location, + title, + text, + ...tags && { tags } + }) + } + } + return documents +} diff --git a/src/assets/javascripts/integrations/search/highlighter/index.ts b/src/assets/javascripts/integrations/search/highlighter/index.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { SearchIndexConfig } from "../_" + +/* ---------------------------------------------------------------------------- + * 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 index configuration + * @param escape - Whether to escape HTML + * + * @returns Search highlight factory function + */ +export function setupSearchHighlighter( + config: SearchIndexConfig, escape: boolean +): SearchHighlightFactoryFn { + const separator = new RegExp(config.separator, "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 => ( + escape + ? escapeHTML(value) + : value + ) + .replace(match, highlight) + .replace(/<\/mark>(\s+)<mark[^>]*>/img, "$1") + } +} diff --git a/src/assets/javascripts/integrations/search/index.ts b/src/assets/javascripts/integrations/search/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 "./document" +export * from "./highlighter" +export * from "./options" +export * from "./query" +export * from "./worker" diff --git a/src/assets/javascripts/integrations/search/options/index.ts b/src/assets/javascripts/integrations/search/options/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 pipeline function + */ +export type SearchPipelineFn = + | "trimmer" /* Trimmer */ + | "stopWordFilter" /* Stop word filter */ + | "stemmer" /* Stemmer */ + +/** + * Search pipeline + */ +export type SearchPipeline = SearchPipelineFn[] + +/* ------------------------------------------------------------------------- */ + +/** + * Search options + */ +export interface SearchOptions { + pipeline: SearchPipeline /* Search pipeline */ + suggestions: boolean /* Search suggestions */ +} diff --git a/src/assets/javascripts/integrations/search/query/_/.eslintrc b/src/assets/javascripts/integrations/search/query/_/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } +} diff --git a/src/assets/javascripts/integrations/search/query/_/index.ts b/src/assets/javascripts/integrations/search/query/_/index.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 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 + * ------------------------------------------------------------------------- */ + +/** + * Parse a search query for analysis + * + * @param value - Query value + * + * @returns Search query clauses + */ +export function parseSearchQuery( + value: string +): SearchQueryClause[] { + const query = new (lunr as any).Query(["title", "text"]) + const parser = new (lunr as any).QueryParser(value, query) + + /* Parse and return query clauses */ + parser.parse() + 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 as any)) + result[clause.term] = false + + /* Return query terms */ + return result +} diff --git a/src/assets/javascripts/integrations/search/query/index.ts b/src/assets/javascripts/integrations/search/query/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 "./transform" diff --git a/src/assets/javascripts/integrations/search/query/transform/.eslintrc b/src/assets/javascripts/integrations/search/query/transform/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "no-control-regex": "off" + } +} diff --git a/src/assets/javascripts/integrations/search/query/transform/index.ts b/src/assets/javascripts/integrations/search/query/transform/index.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 transformation function + * + * @param value - Query value + * + * @returns Transformed query value + */ +export type SearchTransformFn = (value: string) => string + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Default transformation function + * + * 1. Search for terms in quotation marks and prepend a `+` modifier to denote + * that the resulting document must contain all terms, converting the query + * to an `AND` query (as opposed to the default `OR` behavior). While users + * may expect terms 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 terms to an `AND` query. + * + * 2. 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. + * + * 3. Trim excess whitespace from left and right. + * + * @param query - Query value + * + * @returns Transformed query value + */ +export function defaultTransform(query: string): string { + return query + .split(/"([^"]+)"/g) /* => 1 */ + .map((terms, index) => index & 1 + ? terms.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +") + : terms + ) + .join("") + .replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") /* => 2 */ + .trim() /* => 3 */ +} diff --git a/src/assets/javascripts/integrations/search/worker/_/index.ts b/src/assets/javascripts/integrations/search/worker/_/index.ts @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE 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, + from, + map, + share +} from "rxjs" + +import { configuration, feature, translation } from "~/_" +import { WorkerHandler, watchWorker } from "~/browser" + +import { SearchIndex } from "../../_" +import { + SearchOptions, + SearchPipeline +} from "../../options" +import { + SearchMessage, + SearchMessageType, + SearchSetupMessage, + isSearchResultMessage +} from "../message" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search worker + */ +export type SearchWorker = WorkerHandler<SearchMessage> + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Set up search index + * + * @param data - Search index + * + * @returns Search index + */ +function setupSearchIndex({ config, docs }: SearchIndex): SearchIndex { + + /* Override default language with value from translation */ + if (config.lang.length === 1 && config.lang[0] === "en") + config.lang = [ + translation("search.config.lang") + ] + + /* Override default separator with value from translation */ + if (config.separator === "[\\s\\-]+") + config.separator = translation("search.config.separator") + + /* Set pipeline from translation */ + const pipeline = translation("search.config.pipeline") + .split(/\s*,\s*/) + .filter(Boolean) as SearchPipeline + + /* Determine search options */ + const options: SearchOptions = { + pipeline, + suggestions: feature("search.suggest") + } + + /* Return search index after defaulting */ + return { config, docs, options } +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Set up search worker + * + * This function creates a web worker to set up and query the search index, + * which is done using Lunr.js. The index must be passed as an observable to + * enable hacks like _localsearch_ via search index embedding as JSON. + * + * @param url - Worker URL + * @param index - Search index observable input + * + * @returns Search worker + */ +export function setupSearchWorker( + url: string, index: ObservableInput<SearchIndex> +): SearchWorker { + const config = configuration() + const worker = new Worker(url) + + /* Create communication channels and resolve relative links */ + const tx$ = new Subject<SearchMessage>() + const rx$ = watchWorker(worker, { tx$ }) + .pipe( + map(message => { + if (isSearchResultMessage(message)) { + for (const result of message.data.items) + for (const document of result) + document.location = `${new URL(document.location, config.base)}` + } + return message + }), + share() + ) + + /* Set up search index */ + from(index) + .pipe( + map(data => ({ + type: SearchMessageType.SETUP, + data: setupSearchIndex(data) + } as SearchSetupMessage)) + ) + .subscribe(tx$.next.bind(tx$)) + + /* Return search worker */ + return { tx$, rx$ } +} diff --git a/src/assets/javascripts/integrations/search/worker/index.ts b/src/assets/javascripts/integrations/search/worker/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/integrations/search/worker/main/.eslintrc b/src/assets/javascripts/integrations/search/worker/main/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/no-misused-promises": "off" + } +} diff --git a/src/assets/javascripts/integrations/search/worker/main/index.ts b/src/assets/javascripts/integrations/search/worker/main/index.ts @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE 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 "~/polyfills" + +import { Search, SearchIndexConfig } from "../../_" +import { + SearchMessage, + SearchMessageType +} from "../message" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Add support for usage with `iframe-worker` polyfill + * + * While `importScripts` is synchronous when executed inside of a web worker, + * it's not possible to provide a synchronous polyfilled implementation. The + * cool thing is that awaiting a non-Promise is a noop, so extending the type + * definition to return a `Promise` shouldn't break anything. + * + * @see https://bit.ly/2PjDnXi - GitHub comment + */ +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 through the search index 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 index configuration + * + * @returns Promise resolving with no result + */ +async function setupSearchLanguages( + config: SearchIndexConfig +): Promise<void> { + let base = "../lunr" + + /* Detect `iframe-worker` and fix base URL */ + if (typeof parent !== "undefined" && "IFrameWorker" in parent) { + const worker = document.querySelector<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: + return { + type: SearchMessageType.RESULT, + data: index ? index.search(message.data) : { items: [] } + } + + /* All other messages */ + default: + throw new TypeError("Invalid message type") + } +} + +/* ---------------------------------------------------------------------------- + * Worker + * ------------------------------------------------------------------------- */ + +/* @ts-expect-error - 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/assets/javascripts/integrations/search/worker/message/index.ts b/src/assets/javascripts/integrations/search/worker/message/index.ts @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE 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 { SearchIndex, SearchResult } from "../../_" + +/* ---------------------------------------------------------------------------- + * 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 setup messages + * + * @param message - Search worker message + * + * @returns Test result + */ +export function isSearchSetupMessage( + message: SearchMessage +): message is SearchSetupMessage { + return message.type === SearchMessageType.SETUP +} + +/** + * 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 query messages + * + * @param message - Search worker message + * + * @returns Test result + */ +export function isSearchQueryMessage( + message: SearchMessage +): message is SearchQueryMessage { + return message.type === SearchMessageType.QUERY +} + +/** + * 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/assets/javascripts/integrations/sitemap/index.ts b/src/assets/javascripts/integrations/sitemap/index.ts @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 loading 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/assets/javascripts/integrations/version/.eslintrc b/src/assets/javascripts/integrations/version/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "no-null/no-null": "off" + } +} diff --git a/src/assets/javascripts/integrations/version/index.ts b/src/assets/javascripts/integrations/version/index.ts @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 loading. We need to refactor this + // at some point together with instant loading. + // + // 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) + ? new URL(`../${version}/${path}`, config.base) + : new URL(url) + }) + ) + }) + ) + ) + ) + .subscribe(url => setLocation(url)) + + /* 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 loading */ + document$.pipe(switchMap(() => current$)) + .subscribe(current => { + + /* Check if version state was already determined */ + let outdated = __md_get("__outdated", sessionStorage) + if (outdated === null) { + const latest = config.version?.default || "latest" + outdated = !current.aliases.includes(latest) + + /* 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/assets/javascripts/patches/indeterminate/index.ts b/src/assets/javascripts/patches/indeterminate/index.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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>( + // @todo `data-md-state` is deprecated and removed in v9 + ".md-toggle--indeterminate, [data-md-state=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/assets/javascripts/patches/index.ts b/src/assets/javascripts/patches/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/patches/scrollfix/index.ts b/src/assets/javascripts/patches/scrollfix/index.ts @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/patches/scrolllock/index.ts b/src/assets/javascripts/patches/scrolllock/index.ts @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/polyfills/index.ts b/src/assets/javascripts/polyfills/index.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 !== "object") + 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/assets/javascripts/templates/annotation/index.tsx b/src/assets/javascripts/templates/annotation/index.tsx @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 an empty annotation + * + * @param id - Annotation identifier + * + * @returns Element + */ +export function renderAnnotation(id: number): HTMLElement { + return ( + <aside class="md-annotation" tabIndex={0}> + <div class="md-annotation__inner md-tooltip"> + <div class="md-tooltip__inner md-typeset"></div> + </div> + <span class="md-annotation__index"> + <span data-md-annotation-id={id}></span> + </span> + </aside> + ) +} diff --git a/src/assets/javascripts/templates/clipboard/index.tsx b/src/assets/javascripts/templates/clipboard/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/templates/index.ts b/src/assets/javascripts/templates/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/templates/search/index.tsx b/src/assets/javascripts/templates/search/index.tsx @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { feature, translation } from "~/_" +import { + SearchDocument, + SearchMetadata, + SearchResultItem +} from "~/integrations/search" +import { h, truncate } 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: SearchDocument & SearchMetadata, 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 url = new URL(document.location) + 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 */ + return ( + <a href={`${url}`} class="md-search-result__link" tabIndex={-1}> + <article + class={["md-search-result__article", ...parent + ? ["md-search-result__article--document"] + : [] + ].join(" ")} + data-md-score={document.score.toFixed(2)} + > + {parent > 0 && <div class="md-search-result__icon md-icon"></div>} + <h1 class="md-search-result__title">{document.title}</h1> + {teaser > 0 && document.text.length > 0 && + <p class="md-search-result__teaser"> + {truncate(document.text, 320)} + </p> + } + {document.tags && document.tags.map(tag => ( + <span class="md-tag">{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: SearchResultItem +): HTMLElement { + const threshold = result[0].score + const docs = [...result] + + /* Find and extract parent article */ + const parent = docs.findIndex(doc => !doc.location.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}> + {more.length > 0 && more.length === 1 + ? translation("search.result.more.one") + : translation("search.result.more.other", more.length) + } + </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/assets/javascripts/templates/source/index.tsx b/src/assets/javascripts/templates/source/index.tsx @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/templates/tabbed/index.tsx b/src/assets/javascripts/templates/tabbed/index.tsx @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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}></button> + </div> + ) +} diff --git a/src/assets/javascripts/templates/table/index.tsx b/src/assets/javascripts/templates/table/index.tsx @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/assets/javascripts/templates/version/index.tsx b/src/assets/javascripts/templates/version/index.tsx @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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.title")} + > + {active.title} + </button> + <ul class="md-version__list"> + {versions.map(renderVersion)} + </ul> + </div> + ) +} diff --git a/src/assets/javascripts/utilities/h/.eslintrc b/src/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/assets/javascripts/utilities/h/index.ts b/src/assets/javascripts/utilities/h/index.ts @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 = + | 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/assets/javascripts/utilities/index.ts b/src/assets/javascripts/utilities/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 "./string" diff --git a/src/assets/javascripts/utilities/string/index.ts b/src/assets/javascripts/utilities/string/index.ts @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 + * ------------------------------------------------------------------------- */ + +/** + * Truncate a string after the given number of characters + * + * This is not a very reasonable approach, since the summaries kind of suck. + * It would be better to create something more intelligent, highlighting the + * search occurrences and making a better summary out of it, but this note was + * written three years ago, so who knows if we'll ever fix it. + * + * @param value - Value to be truncated + * @param n - Number of characters + * + * @returns Truncated value + */ +export function truncate(value: string, n: number): string { + let i = n + if (value.length > i) { + while (value[i] !== " " && --i > 0) { /* keep eating */ } + return `${value.substring(0, i)}...` + } + return value +} + +/** + * 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/assets/javascripts/workers/search.ts b/src/assets/javascripts/workers/search.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE 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/assets/stylesheets/_config.scss b/src/assets/stylesheets/_config.scss @@ -0,0 +1,42 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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(479px), + landscape: px2em(480px) px2em(719px) + ), + tablet: ( + portrait: px2em(720px) px2em(959px), + landscape: px2em(960px) px2em(1219px) + ), + screen: ( + small: px2em(1220px) px2em(1599px), + medium: px2em(1600px) px2em(1999px), + large: px2em(2000px) + ) +); diff --git a/src/assets/stylesheets/main.scss b/src/assets/stylesheets/main.scss @@ -0,0 +1,80 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/layout/banner"; +@import "main/layout/base"; +@import "main/layout/clipboard"; +@import "main/layout/consent"; +@import "main/layout/content"; +@import "main/layout/dialog"; +@import "main/layout/feedback"; +@import "main/layout/footer"; +@import "main/layout/form"; +@import "main/layout/header"; +@import "main/layout/nav"; +@import "main/layout/search"; +@import "main/layout/select"; +@import "main/layout/sidebar"; +@import "main/layout/source"; +@import "main/layout/tabs"; +@import "main/layout/tag"; +@import "main/layout/tooltip"; +@import "main/layout/top"; +@import "main/layout/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/assets/stylesheets/main/_colors.scss b/src/assets/stylesheets/main/_colors.scss @@ -0,0 +1,134 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; +} + +// ---------------------------------------------------------------------------- + +// Allow to explicitly use color schemes in nested content +[data-md-color-scheme="default"] { + @extend %root; +} + +// ---------------------------------------------------------------------------- +// Placeholders +// ---------------------------------------------------------------------------- + +// Default theme, i.e. light mode +%root { + + // 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); + + // 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); + + // Code color shades + --md-code-fg-color: hsla(200, 18%, 26%, 1); + --md-code-bg-color: hsla(0, 0%, 96%, 1); + + // Code highlighting color shades + --md-code-hl-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5); + --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 `mark` color shades + --md-typeset-mark-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5); + + // 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 `table` color shades + --md-typeset-table-color: hsla(0, 0%, 0%, 0.12); + + // 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-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.3); + --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/assets/stylesheets/main/_icons.scss b/src/assets/stylesheets/main/_icons.scss @@ -0,0 +1,37 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/assets/stylesheets/main/_modifiers.scss b/src/assets/stylesheets/main/_modifiers.scss @@ -0,0 +1,58 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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: left; + width: px2rem(234px); + margin-top: 0; + margin-inline-end: px2rem(16px); + margin-bottom: px2rem(16px); + + // Adjust for right-to-left languages + [dir="rtl"] & { + float: right; + } + + // Modifier to move to end (ltr: right, rtl: left) + &.end { + float: right; + margin-inline: px2rem(16px) 0; + + // Adjust for right-to-left languages + [dir="rtl"] & { + float: left; + } + } + } + } +} diff --git a/src/assets/stylesheets/main/_resets.scss b/src/assets/stylesheets/main/_resets.scss @@ -0,0 +1,118 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 { + display: block; + box-sizing: content-box; + 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-collapse: separate; + border-spacing: 0; +} + +// Reset table cell styles +td, +th { + font-weight: 400; + vertical-align: top; +} + +// Reset button styles +button { + margin: 0; + padding: 0; + font-size: inherit; + font-family: inherit; + background: transparent; + border: 0; +} + +// Reset input styles +input { + border: 0; + outline: none; +} diff --git a/src/assets/stylesheets/main/_typeset.scss b/src/assets/stylesheets/main/_typeset.scss @@ -0,0 +1,619 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 { + color: var(--md-typeset-color); + font-feature-settings: "kern", "liga"; + font-family: var(--md-text-font-family); +} + +// Define monospaced fonts +code, +pre, +kbd { + color: var(--md-typeset-color); + font-feature-settings: "kern"; + font-family: var(--md-code-font-family); +} + +// ---------------------------------------------------------------------------- +// 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); + color: var(--md-default-fg-color--light); + font-weight: 300; + font-size: px2em(32px); + line-height: 1.3; + letter-spacing: -0.01em; + } + + // Headline on level 2 + h2 { + margin: px2em(40px, 25px) 0 px2em(16px, 25px); + font-weight: 300; + font-size: px2em(25px); + line-height: 1.4; + letter-spacing: -0.01em; + } + + // Headline on level 3 + h3 { + margin: px2em(32px, 20px) 0 px2em(16px, 20px); + font-weight: 400; + font-size: px2em(20px); + 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; + color: var(--md-default-fg-color--light); + font-weight: 700; + font-size: px2em(12.8px); + 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 + &: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 { + 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; + margin: 0; + padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px); + overflow: auto; + word-break: normal; + outline-color: var(--md-accent-fg-color); + box-shadow: none; + box-decoration-break: slice; + touch-action: auto; + 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); + color: var(--md-default-fg-color); + font-size: px2em(12px); + vertical-align: text-top; + word-break: break-word; + 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; + border-bottom: px2rem(1px) dotted var(--md-default-fg-color--light); + cursor: help; + + // Show tooltip for touch devices + @media (hover: none) { + position: relative; + + // Tooltip + &[title]:is(:focus, :hover)::after { + position: absolute; + inset-inline-start: 0; + display: inline-block; + width: auto; + min-width: max-content; + max-width: 80%; + margin-top: 2em; + padding: px2rem(4px) px2rem(6px); + color: var(--md-default-bg-color); + font-size: px2rem(14px); + background-color: var(--md-default-fg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z3); + content: attr(title); + } + } + } + + // 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 { + margin-inline-start: px2em(10px); + padding: 0; + + // 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-bottom: 0.5em; + margin-inline-start: px2em(20px); + + // 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; + } + + // Hide images for dark mode + &[src$="#only-dark"], + &[src$="#gh-dark-mode-only"] { + display: none; + } + } + + // 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); + background-color: var(--md-default-bg-color); + border: px2rem(1px) solid var(--md-typeset-table-color); + border-radius: px2rem(2px); + touch-action: auto; + + // [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; + + // Links in table headings + a { + color: inherit; + } + } + + // 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: rgba(0, 0, 0, 0.035); + 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; + mask-image: var(--md-typeset-table-sort-icon); + mask-repeat: no-repeat; + mask-size: contain; + transition: background-color 125ms; + content: ""; + } + + // 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; + margin-bottom: 0.5em; + padding: 0 px2rem(16px); + + // [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/assets/stylesheets/main/extensions/markdown/_admonition.scss b/src/assets/stylesheets/main/extensions/markdown/_admonition.scss @@ -0,0 +1,183 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 $clr-blue-a200, + abstract summary tldr: clipboard-text $clr-light-blue-a400, + info todo: information $clr-cyan-a700, + tip hint important: fire $clr-teal-a700, + success check done: check-bold $clr-green-a700, + question help faq: help-circle $clr-light-green-a700, + warning caution attention: alert $clr-orange-a400, + failure fail missing: close-thick $clr-red-a200, + danger error: lightning-bolt $clr-red-a400, + bug: bug $clr-pink-a400, + example: format-list-numbered $clr-deep-purple-a200, + quote cite: format-quote-close $clr-grey +) !default; + +// ---------------------------------------------------------------------------- +// Rules: layout +// ---------------------------------------------------------------------------- + +// Admonition variables +:root { + @each $names, $props in $admonitions { + --md-admonition-icon--#{nth($names, 1)}: + svg-load("material/#{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. + :is(.admonition, details) { + display: flow-root; + margin: px2em(20px, 12.8px) 0; + padding: 0 px2rem(12px); + color: var(--md-admonition-fg-color); + font-size: px2rem(12.8px); + page-break-inside: avoid; + background-color: var(--md-admonition-bg-color); + border: 0 solid $clr-blue-a200; + border-inline-start-width: px2rem(4px); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z1); + + // [print]: Omit shadow as it may lead to rendering errors + @media print { + box-shadow: none; + } + + // 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 + :is(.admonition, details) { + 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 + :is(.admonition-title, summary) { + position: relative; + margin-block: 0; + margin-inline: px2rem(-16px) px2rem(-12px); + padding-block: px2rem(8px); + padding-inline: px2rem(44px) 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); + inset-inline-start: px2rem(16px); + width: px2rem(20px); + height: px2rem(20px); + background-color: $clr-blue-a200; + mask-image: var(--md-admonition-icon--note); + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: flavours +// ---------------------------------------------------------------------------- + +// Define admonition flavors +@each $names, $props in $admonitions { + $name: list.nth($names, 1); + $tint: list.nth($props, 2); + + // Admonition flavour selectors + $flavours: (); + @each $name in $names { + $flavours: list.join($flavours, ".#{$name}", $separator: comma); + } + + // Admonition flavour + .md-typeset :is(.admonition, details):is(#{$flavours}) { + border-color: $tint; + } + + // Admonition flavour title + .md-typeset :is(#{$flavours}) > :is(.admonition-title, summary) { + background-color: color.adjust($tint, $alpha: -0.9); + + // Admonition icon + &::before { + background-color: $tint; + mask-image: var(--md-admonition-icon--#{$name}); + mask-repeat: no-repeat; + mask-size: contain; + } + } +} diff --git a/src/assets/stylesheets/main/extensions/markdown/_footnotes.scss b/src/assets/stylesheets/main/extensions/markdown/_footnotes.scss @@ -0,0 +1,145 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 { + color: var(--md-default-fg-color--light); + font-size: px2rem(12.8px); + + // 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 { + transform: translateX(0); + opacity: 1; + transition: none; + } + + // Show backreferences on footnote hover/target + &:is(:hover, :target) .footnote-backref { + transform: translateX(0); + opacity: 1; + } + + // Adjust spacing on first child + > :first-child { + margin-top: 0; + } + } + } + } + + // Footnote reference + .footnote-ref { + font-weight: 700; + font-size: px2em(12px, 16px); + + // 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; + color: var(--md-typeset-a-color); + // Hack: omit Unicode arrow for replacement with icon + font-size: 0; + vertical-align: text-bottom; + transform: translateX(px2rem(5px)); + opacity: 0; + transition: + color 250ms, + transform 250ms 250ms, + opacity 125ms 250ms; + + // [print]: Show footnote backreferences + @media print { + color: var(--md-typeset-a-color); + transform: translateX(0); + opacity: 1; + } + + // 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); + background-color: currentcolor; + mask-image: var(--md-footnotes-icon); + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + + // Adjust for right-to-left languages + [dir="rtl"] & { + + // Flip icon vertically + svg { + transform: scaleX(-1); + } + } + } + } +} diff --git a/src/assets/stylesheets/main/extensions/markdown/_toc.scss b/src/assets/stylesheets/main/extensions/markdown/_toc.scss @@ -0,0 +1,92 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss b/src/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss @@ -0,0 +1,52 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + margin-inline: auto !important; // stylelint-disable-line + padding: 0 px2rem(16px); + touch-action: auto; + + // MathJax container - see https://bit.ly/3HR8YJ5 + mjx-container { + margin: 0 !important; // stylelint-disable-line + } + } + } +} diff --git a/src/assets/stylesheets/main/extensions/pymdownx/_critic.scss b/src/assets/stylesheets/main/extensions/pymdownx/_critic.scss @@ -0,0 +1,78 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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, addition or comment + :is(del, ins, .comment).critic { + box-decoration-break: clone; + } + + // Deletion + del.critic { + background-color: var(--md-typeset-del-color); + } + + // Addition + ins.critic { + background-color: var(--md-typeset-ins-color); + } + + // Comment + .critic.comment { + color: var(--md-code-hl-comment-color); + + // Comment opening mark + &::before { + content: "/* "; + } + + // Comment closing mark + &::after { + content: " */"; + } + } + + // Critic block + .critic.block { + display: block; + margin: 1em 0; + padding-inline: px2rem(16px); + 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/assets/stylesheets/main/extensions/pymdownx/_details.scss b/src/assets/stylesheets/main/extensions/pymdownx/_details.scss @@ -0,0 +1,116 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 { + 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 { + display: block; + min-height: px2rem(20px); + padding-inline-end: px2rem(36px); + border-start-start-radius: px2rem(2px); + border-start-end-radius: px2rem(2px); + cursor: pointer; + + // 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); + inset-inline-end: px2rem(8px); + width: px2rem(20px); + height: px2rem(20px); + background-color: currentcolor; + mask-image: var(--md-details-icon); + mask-repeat: no-repeat; + mask-size: contain; + transform: rotate(0deg); + transition: transform 250ms; + content: ""; + + // 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/assets/stylesheets/main/extensions/pymdownx/_emoji.scss b/src/assets/stylesheets/main/extensions/pymdownx/_emoji.scss @@ -0,0 +1,43 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/assets/stylesheets/main/extensions/pymdownx/_highlight.scss b/src/assets/stylesheets/main/extensions/pymdownx/_highlight.scss @@ -0,0 +1,381 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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) { + margin: 0 px2em(-2px); + padding: 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; + margin: 0 px2em(-16px, 13.6px); + padding: 0 px2em(16px, 13.6px); + background-color: var(--md-code-hl-color); + } + + // Code block title + span.filename { + position: relative; + display: flow-root; + margin-top: 1em; + padding: px2em(9px, 13.6px) px2em(16px, 13.6px); + font-weight: 700; + font-size: px2em(13.6px); + 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; + margin-right: px2em(16px, 13.6px); + margin-left: px2em(-16px, 13.6px); + padding-left: px2em(16px, 13.6px); + color: var(--md-default-fg-color--light); + background-color: var(--md-code-bg-color); + box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lightest) inset; + content: attr(data-linenos); + user-select: none; + } + + // 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); + background-color: var(--md-code-bg-color); + border-top-left-radius: px2rem(2px); + border-bottom-left-radius: px2rem(2px); + user-select: none; + } + + // 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 { + margin-top: calc(-1em + #{px2em(-2px)}); + padding: 0 px2em(16px); + 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/assets/stylesheets/main/extensions/pymdownx/_keys.scss b/src/assets/stylesheets/main/extensions/pymdownx/_keys.scss @@ -0,0 +1,115 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss b/src/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss @@ -0,0 +1,393 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + } + } + } + } + } + + // 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; + background: var(--md-accent-fg-color); + transform: translateX(var(--md-indicator-x)); + transition: + width 225ms, + transform 250ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + content: ""; + } + } + } + + // 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); + color: var(--md-default-fg-color--light); + font-weight: 700; + font-size: px2rem(12.8px); + white-space: nowrap; + border-bottom: px2rem(2px) solid transparent; + border-radius: px2rem(2px) px2rem(2px) 0 0; + cursor: pointer; + 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-accent-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); + border-radius: 100%; + cursor: pointer; + transition: background-color 250ms; + pointer-events: initial; + + // 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%; + background-color: currentcolor; + transition: + background-color 250ms, + transform 250ms; + mask-image: var(--md-tabbed-icon--prev); + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + } + } + + // Tabbed control + .tabbed-control { + position: absolute; + display: flex; + justify-content: start; + width: px2rem(24px); + height: px2rem(38px); + background: + linear-gradient( + to right, + var(--md-default-bg-color) 60%, + transparent + ); + transition: opacity 125ms; + pointer-events: none; + + // 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; + margin: 0 px2rem(-16px); + padding-inline-start: 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); + margin-inline-start: px2rem(-16px); + padding-inline-start: px2rem(16px); + } + + // Tabbed control next + ~ .tabbed-control--next { + width: px2rem(40px); + margin-inline-end: px2rem(-16px); + padding-inline-end: px2rem(16px); + } + } +} + +// ---------------------------------------------------------------------------- +// Placeholders: improve colocation for better compression +// ---------------------------------------------------------------------------- + +// Tab label placeholder +%tabbed-label { + + // [screen]: Show active state + @media screen { + color: var(--md-accent-fg-color); + + // [no-js]: Show border (indicator is animated with JavaScript) + .no-js & { + border-color: var(--md-accent-fg-color); + } + } +} + +// Tab label on keyboard focus placeholder +%tabbed-label-focus-visible { + background-color: var(--md-accent-fg-color--transparent); +} + +// Tab content placeholder +%tabbed-content { + display: block; +} diff --git a/src/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss b/src/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss @@ -0,0 +1,79 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + inset-inline-start: px2em(-24px); + width: px2em(20px); + height: px2em(20px); + background-color: var(--md-default-fg-color--lightest); + mask-image: var(--md-tasklist-icon); + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + } + + // 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/assets/stylesheets/main/integrations/_mermaid.scss b/src/assets/stylesheets/main/integrations/_mermaid.scss @@ -0,0 +1,43 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 +// ---------------------------------------------------------------------------- + +// All definitions +:root > * { + --md-mermaid-font-family: var(--md-text-font-family), sans-serif; + + // 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); +} + +// Mermaid container +.mermaid { + margin: 1em 0; + line-height: normal; +} diff --git a/src/assets/stylesheets/main/layout/_banner.scss b/src/assets/stylesheets/main/layout/_banner.scss @@ -0,0 +1,63 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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-default-fg-color); + background: var(--md-typeset-mark-color); + } + + // Banner wrapper + &__inner { + margin: px2rem(12px) auto; + padding: 0 px2rem(16px); + font-size: px2rem(14px); + } + + // Banner button + &__button { + float: right; + color: inherit; + cursor: pointer; + transition: opacity 250ms; + + // Button on hover + &:hover { + opacity: 0.7; + } + } +} diff --git a/src/assets/stylesheets/main/layout/_base.scss b/src/assets/stylesheets/main/layout/_base.scss @@ -0,0 +1,183 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + white-space: nowrap; + 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; + margin: px2rem(10px); + padding: px2rem(6px) px2rem(10px); + color: var(--md-default-bg-color); + font-size: px2rem(12.8px); + background-color: var(--md-default-fg-color); + border-radius: px2rem(2px); + outline-color: var(--md-accent-fg-color); + transform: translateY(px2rem(8px)); + opacity: 0; + + // Show skip link on focus + &:focus { + z-index: 10; + transform: translateY(0); + opacity: 1; + transition: + transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 175ms 75ms; + } +} + +// ---------------------------------------------------------------------------- +// Rules: print styles +// ---------------------------------------------------------------------------- + +// Add margins to page +@page { + margin: 25mm; +} diff --git a/src/assets/stylesheets/main/layout/_clipboard.scss b/src/assets/stylesheets/main/layout/_clipboard.scss @@ -0,0 +1,101 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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); + border-radius: px2rem(2px); + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(2px); + cursor: pointer; + 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; + background-color: currentcolor; + mask-image: var(--md-clipboard-icon); + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + } + + // 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/assets/stylesheets/main/layout/_consent.scss b/src/assets/stylesheets/main/layout/_consent.scss @@ -0,0 +1,127 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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% { + transform: translateY(100%); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +// 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/assets/stylesheets/main/layout/_content.scss b/src/assets/stylesheets/main/layout/_content.scss @@ -0,0 +1,102 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 { + margin: 0 px2rem(16px) px2rem(24px); + padding-top: px2rem(12px); + + // [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: right; + margin: px2rem(8px) 0; + margin-inline-start: px2rem(8px); + padding: 0; + + // [print]: Hide buttons + @media print { + display: none; + } + + // Adjust for right-to-left languages + [dir="rtl"] & { + float: left; + } + + // 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/assets/stylesheets/main/layout/_dialog.scss b/src/assets/stylesheets/main/layout/_dialog.scss @@ -0,0 +1,65 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + inset-inline-end: px2rem(16px); + bottom: px2rem(16px); + z-index: 4; + min-width: px2rem(222px); + padding: px2rem(8px) px2rem(12px); + background-color: var(--md-default-fg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z3); + transform: translateY(100%); + opacity: 0; + transition: + transform 0ms 400ms, + opacity 400ms; + pointer-events: none; + + // [print]: Hide dialog + @media print { + display: none; + } + + // Active dialog + &--active { + transform: translateY(0); + opacity: 1; + transition: + transform 400ms cubic-bezier(0.075, 0.85, 0.175, 1), + opacity 400ms; + pointer-events: initial; + } + + // Dialog wrapper + &__inner { + color: var(--md-default-bg-color); + font-size: px2rem(14px); + } +} diff --git a/src/assets/stylesheets/main/layout/_feedback.scss b/src/assets/stylesheets/main/layout/_feedback.scss @@ -0,0 +1,110 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 { + margin: 0; + padding: 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; + transform: translateY(px2rem(8px)); + opacity: 0; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + + // Feedback note value + > * { + max-width: px2rem(320px); + margin: 0 auto; + } + + // Show after submission + :disabled & { + transform: translateY(0); + opacity: 1; + } + } +} diff --git a/src/assets/stylesheets/main/layout/_footer.scss b/src/assets/stylesheets/main/layout/_footer.scss @@ -0,0 +1,202 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + padding-top: px2rem(28px); + padding-bottom: 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); + } + + // Footer link to previous page + &--prev { + + // [mobile -]: Adjust width to 25/75 and hide title + @include break-to-device(mobile) { + + // 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 { + position: relative; + flex-grow: 1; + max-width: calc(100% - #{px2rem(48px)}); + padding: 0 px2rem(20px); + font-size: px2rem(18px); + line-height: px2rem(48px); + white-space: nowrap; + } + + // Footer link button + &__button { + margin: px2rem(4px); + padding: px2rem(8px); + } + + // Footer link direction (i.e. prev and next) + &__direction { + position: absolute; + inset-inline: 0; + margin-top: px2rem(-20px); + padding: 0 px2rem(20px); + 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%; + margin: auto px2rem(12px); + padding: px2rem(8px) 0; + color: var(--md-footer-fg-color--lighter); + font-size: px2rem(12.8px); + + // [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 { + margin: 0 px2rem(8px); + padding: px2rem(4px) 0 px2rem(12px); + + // [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/assets/stylesheets/main/layout/_form.scss b/src/assets/stylesheets/main/layout/_form.scss @@ -0,0 +1,83 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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); + color: var(--md-primary-fg-color); + font-weight: 700; + border: px2rem(2px) solid currentcolor; + border-radius: px2rem(2px); + cursor: pointer; + 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/assets/stylesheets/main/layout/_header.scss b/src/assets/stylesheets/main/layout/_header.scss @@ -0,0 +1,262 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + top: 0; + inset-inline: 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] { + transform: translateY(-100%); + transition: + transform 250ms cubic-bezier(0.8, 0, 0.6, 1), + box-shadow 250ms; + } + + // 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; + margin: px2rem(4px); + padding: px2rem(8px); + color: currentcolor; + vertical-align: middle; + outline-color: var(--md-accent-fg-color); + cursor: pointer; + 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 { + margin: px2rem(4px); + padding: px2rem(8px); + + // [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; + transform: translateX(px2rem(25px)); + opacity: 0; + transition: + transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1), + opacity 150ms; + pointer-events: none; + + // 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-end: px2rem(8px); + margin-inline-start: px2rem(20px); + 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; + transform: translateX(px2rem(-25px)); + opacity: 0; + transition: + transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1), + opacity 150ms; + pointer-events: none; + + // 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; + transform: translateX(0); + opacity: 1; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + pointer-events: initial; + } + } + + // 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; + } + } + + // 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/assets/stylesheets/main/layout/_nav.scss b/src/assets/stylesheets/main/layout/_nav.scss @@ -0,0 +1,642 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + 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 { + margin: 0; + padding: 0; + list-style: none; + } + + // Navigation item + &__item { + padding: 0 px2rem(12px); + + // Navigation item on level 2 + & & { + padding-inline-end: 0; + } + } + + // Navigation link + &__link { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 0.625em; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + 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 { + color: var(--md-typeset-a-color); + } + + // Stretch section index link to full width + .md-nav__item &--index [href] { + width: 100%; + } + + // Navigation link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + + // 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 link children (for section indexes) + > * { + display: flex; + cursor: pointer; + } + } + + // 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; + top: 0; + inset-inline: 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); + color: var(--md-default-fg-color--light); + line-height: px2rem(48px); + white-space: nowrap; + background-color: var(--md-default-fg-color--lightest); + cursor: pointer; + + // 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%; + background-color: currentcolor; + mask-image: var(--md-nav-icon--prev); + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + } + } + + // Navigation list + ~ .md-nav__list { + overflow-y: auto; + 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; + touch-action: pan-y; + + // Omit border on first child + > :first-child { + border-top: 0; + } + } + + // Top-level navigation title + &[for="__drawer"] { + color: var(--md-primary-bg-color); + font-weight: 700; + background-color: var(--md-primary-fg-color); + } + + // Button with logo, pointing to `config.site_url` + .md-logo { + position: absolute; + top: px2rem(4px); + inset-inline: px2rem(4px); + display: block; + margin: px2rem(4px); + padding: px2rem(8px); + } + } + + // Navigation list + .md-nav__list { + flex: 1; + } + + // Navigation item + .md-nav__item { + padding: 0; + 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 { + margin-top: 0; + padding: px2rem(12px) px2rem(16px); + + // 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%; + background-color: currentcolor; + mask-image: var(--md-nav-icon--next); + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + } + } + } + + // 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; + } + + // Toggle for nested navigation + &__toggle ~ & { + display: flex; + transform: translateX(100%); + opacity: 0; + transition: + transform 250ms cubic-bezier(0.8, 0, 0.6, 1), + opacity 125ms 50ms; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(-100%); + } + } + + // Show nested navigation when toggle is active + &__toggle:checked ~ & { + transform: translateX(0); + opacity: 1; + transition: + transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 125ms 125ms; + + // 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) { + + // Navigation title + &--secondary &__title { + + // Adjust snapping behavior + &[for="__toc"] { + scroll-snap-align: start; + } + + // Hide navigation icon + .md-nav__icon { + display: none; + } + } + } + + // [screen +]: Tree-like navigation + @include break-from-device(screen) { + transition: max-height 250ms cubic-bezier(0.86, 0, 0.07, 1); + + // Navigation title + &--primary &__title { + + // Adjust snapping behavior + &[for="__drawer"] { + scroll-snap-align: start; + } + + // Hide navigation icon + .md-nav__icon { + display: none; + } + } + + // Hide toggle for nested navigation + &__toggle ~ & { + display: none; + } + + // Show nested navigation when toggle is active or indeterminate + &__toggle:is(:checked, :indeterminate) ~ & { + display: block; + } + + // 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; + pointer-events: none; + + // Make navigation link clickable + &--index [href] { + pointer-events: initial; + } + + // Hide naviation icon + .md-nav__icon { + display: none; + } + } + + // Navigation + > .md-nav { + display: block; + + // Adjust spacing on next level item + > .md-nav__list > .md-nav__item { + padding: 0; + } + } + } + + // Navigation icon + &__icon { + float: right; + width: px2rem(18px); + height: px2rem(18px); + border-radius: 100%; + transition: + background-color 250ms, + transform 250ms; + + // Adjust for right-to-left languages + [dir="rtl"] & { + float: left; + transform: rotate(180deg); + } + + // 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); + background-color: currentcolor; + mask-image: var(--md-nav-icon--next); + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + } + + // 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 nested level 0 navigation items and site title + > .md-nav__list > .md-nav__item--nested, + > .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; + padding: 0; + + // Show navigation link as title + > .md-nav__link { + margin-top: 0; + padding: 0 px2rem(12px); + font-weight: 700; + pointer-events: none; + + // Make navigation link clickable + &--index [href] { + pointer-events: initial; + } + + // Hide naviation icon + .md-nav__icon { + display: none; + } + } + } + } + + // Hack: Always show active navigation tab on breakpoint screen, despite + // of checkbox being checked or not. Fixes #1655. + .md-nav[data-md-level="1"] { + display: block; + + // Adjust spacing for level 1 navigation items + > .md-nav__list > .md-nav__item { + padding-inline-end: px2rem(12px); + } + } + } + + // 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; + border-inline-start: px2rem(1px) solid var(--md-primary-fg-color); + + // Hide table of contents title + > .md-nav__title { + display: none; + } + } + } + } +} diff --git a/src/assets/stylesheets/main/layout/_search.scss b/src/assets/stylesheets/main/layout/_search.scss @@ -0,0 +1,713 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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); + inset-inline-start: px2rem(-44px); + width: px2rem(40px); + height: px2rem(40px); + overflow: hidden; + background-color: var(--md-default-bg-color); + border-radius: px2rem(20px); + transform-origin: center; + transition: + transform 300ms 100ms, + opacity 200ms 200ms; + pointer-events: none; + + // 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; + inset-inline-start: 0; + width: 0; + height: 0; + background-color: hsla(0, 0%, 0%, 0.54); + cursor: pointer; + transition: + width 0ms 250ms, + height 0ms 250ms, + opacity 250ms; + + // 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; + inset-inline-start: 0; + z-index: 2; + width: 0; + height: 0; + overflow: hidden; + transform: translateX(5%); + opacity: 0; + transition: + width 0ms 300ms, + height 0ms 300ms, + transform 150ms 150ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 150ms 150ms; + + // 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%; + transform: translateX(0); + opacity: 1; + transition: + width 0ms 0ms, + height 0ms 0ms, + transform 150ms 150ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms 150ms; + } + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + position: relative; + float: right; + width: px2rem(234px); + padding: px2rem(2px) 0; + transition: width 250ms cubic-bezier(0.1, 0.7, 0.1, 1); + + // Adjust for right-to-left languages + [dir="rtl"] & { + float: left; + } + } + + // 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); + color: inherit; + font-size: px2rem(16px); + + // 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, + &::placeholder { + color: var(--md-default-fg-color--light); + } + } + } + } + + // 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 + > * { + margin-inline-start: px2rem(4px); + color: var(--md-default-fg-color--light); + transform: scale(0.75); + opacity: 0; + transition: + transform 150ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + + // Show reset button when search is active and input non-empty + [data-md-toggle="search"]:checked ~ .md-header + .md-search__input:valid ~ & { + transform: scale(1); + opacity: 1; + pointer-events: initial; + + // 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); + color: var(--md-default-fg-color--lighter); + font-size: px2rem(18px); + 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; + background-color: var(--md-default-bg-color); + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + // 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; + + // 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); + color: var(--md-default-fg-color--light); + font-size: px2rem(12.8px); + line-height: px2rem(36px); + 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 { + margin: 0; + padding: 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 link + &__more summary { + display: block; + padding: px2em(12px) px2rem(16px); + color: var(--md-typeset-a-color); + font-size: px2rem(12.8px); + outline: none; + cursor: pointer; + transition: + color 250ms, + background-color 250ms; + scroll-snap-align: start; + + // [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) { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + } + + // 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; + } + + // Adjust transparency of less relevant results + ~ * > * { + opacity: 0.65; + } + } + + // 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 article document + &--document { + + // Search result title + .md-search-result__title { + margin: px2rem(11px) 0; + font-weight: 400; + font-size: px2rem(16px); + line-height: 1.4; + } + } + } + + // 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%; + background-color: currentcolor; + mask-image: var(--md-search-result-icon); + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: scaleX(-1); + } + } + } + + // Search result title + &__title { + margin: 0.5em 0; + font-weight: 700; + font-size: px2rem(12.8px); + line-height: 1.6; + } + + // Search result teaser + &__teaser { + display: -webkit-box; + max-height: px2rem(40px); + margin: 0.5em 0; + overflow: hidden; + color: var(--md-default-fg-color--light); + font-size: px2rem(12.8px); + line-height: 1.6; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + // [mobile -]: Adjust number of lines + @include break-to-device(mobile) { + max-height: px2rem(60px); + -webkit-line-clamp: 3; + } + + // [tablet landscape]: Adjust number of lines + @include break-at-device(tablet landscape) { + max-height: px2rem(60px); + -webkit-line-clamp: 3; + } + + // Search term highlighting + mark { + text-decoration: underline; + background-color: transparent; + } + } + + // Search result terms + &__terms { + margin: 0.5em 0; + font-size: px2rem(12.8px); + font-style: italic; + } + + // Search term highlighting + mark { + color: var(--md-accent-fg-color); + background-color: transparent; + } +} diff --git a/src/assets/stylesheets/main/layout/_select.scss b/src/assets/stylesheets/main/layout/_select.scss @@ -0,0 +1,115 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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); + transform: translate3d(-50%, px2rem(6px), 0); + opacity: 0; + transition: + transform 250ms 375ms, + opacity 250ms 250ms, + max-height 0ms 500ms; + + // Selection bubble on parent focus/hover + .md-select:is(:focus-within, :hover) & { + max-height: px2rem(200px); + transform: translate3d(-50%, 0, 0); + opacity: 1; + transition: + transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 250ms, + max-height 0ms; + } + + // Selection bubble handle + &::after { + position: absolute; + top: 0; + left: 50%; + width: 0; + height: 0; + margin-top: px2rem(-4px); + margin-left: px2rem(-4px); + border: px2rem(4px) solid transparent; + border-top: 0; + border-bottom-color: var(--md-default-bg-color); + content: ""; + } + } + + // Selection list + &__list { + max-height: inherit; + margin: 0; + padding: 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); + outline: none; + cursor: pointer; + 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/assets/stylesheets/main/layout/_sidebar.scss b/src/assets/stylesheets/main/layout/_sidebar.scss @@ -0,0 +1,181 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + } + + // [tablet -]: Show navigation as drawer + @include break-to-device(tablet) { + + // Primary sidebar with navigation + &--primary { + position: fixed; + top: 0; + inset-inline-start: px2rem(-242px); + z-index: 5; + display: block; + width: px2rem(242px); + height: 100%; + background-color: var(--md-default-bg-color); + transform: translateX(0); + transition: + transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 250ms; + + // 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; + } + } + + // 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-color: var(--md-default-fg-color--lighter) transparent; + + // Sidebar 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); + } + } + } +} + +// [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/assets/stylesheets/main/layout/_source.scss b/src/assets/stylesheets/main/layout/_source.scss @@ -0,0 +1,181 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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% { + transform: translateY(100%); + opacity: 0; + } + + 50% { + opacity: 0; + } + + 100% { + transform: translateY(0%); + opacity: 1; + } +} + +// ---------------------------------------------------------------------------- +// 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-top: px2rem(12px); + margin-inline-start: px2rem(12px); + } + + // Adjust spacing if icon is present + + .md-source__repository { + margin-inline-start: px2rem(-40px); + padding-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%; + margin: px2rem(2px) 0 0; + padding: 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; + background-color: currentcolor; + mask-repeat: no-repeat; + mask-size: contain; + content: ""; + } + + // 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/assets/stylesheets/main/layout/_tabs.scss b/src/assets/stylesheets/main/layout/_tabs.scss @@ -0,0 +1,110 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + color: var(--md-primary-bg-color); + line-height: 1.3; + 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 { + margin: 0; + margin-inline-start: px2rem(4px); + padding: 0; + white-space: nowrap; + list-style: none; + contain: content; + } + + // Navigation tabs item + &__item { + display: inline-block; + height: px2rem(48px); + padding-inline: px2rem(12px); + } + + // Navigation tabs link - could be defined as block elements and aligned via + // line height, but this would imply more repaints when scrolling + &__link { + display: block; + 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; + + // Active link and link on focus/hover + &--active, + &:is(:focus, :hover) { + color: inherit; + opacity: 1; + } + + // 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] & { + transform: translateY(50%); + opacity: 0; + transition: + transform 0ms 100ms, + opacity 100ms; + } + } +} diff --git a/src/assets/stylesheets/main/layout/_tag.scss b/src/assets/stylesheets/main/layout/_tag.scss @@ -0,0 +1,65 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 list +.md-tags { + margin-bottom: px2em(12px); +} + +// Tag +.md-tag { + display: inline-block; + margin-inline-end: 0.5em; + margin-bottom: 0.5em; + padding: px2em(4px, 12.8px) px2em(12px, 12.8px); + font-weight: 700; + font-size: px2rem(12.8px); + line-height: 1.6; + background: var(--md-default-fg-color--lightest); + border-radius: px2rem(8px); + + // Linked tag + &[href] { + color: inherit; + outline: none; + -webkit-tap-highlight-color: transparent; + transition: + color 125ms, + background-color 125ms; + + // Linked tag on focus/hover + &:focus, + &:hover { + color: var(--md-accent-bg-color); + background-color: var(--md-accent-fg-color); + } + } + + // Tag inside headline + [id] > & { + vertical-align: text-top; + } +} diff --git a/src/assets/stylesheets/main/layout/_tooltip.scss b/src/assets/stylesheets/main/layout/_tooltip.scss @@ -0,0 +1,253 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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% { + box-shadow: 0 0 0 0 var(--md-default-fg-color--lightest); + transform: scale(0.95); + } + + 75% { + box-shadow: 0 0 0 px2em(10px) transparent; + transform: scale(1); + } + + 100% { + box-shadow: 0 0 0 0 transparent; + transform: scale(0.95); + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Tooltip variables +:root { + --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)}); + max-height: 0; + color: var(--md-default-fg-color); + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z2); + transform: translateY(px2rem(-8px)); + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + opacity: 0; + transition: + transform 0ms 250ms, + opacity 250ms, + max-height 0ms 250ms, + z-index 250ms; + + // Tooltip on parent focus + :focus-within > & { + max-height: 1000%; + transform: translateY(0); + opacity: 1; + transition: + transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 250ms, + max-height 250ms, + z-index 0ms; + } + + // Show outline for keyboard devices + .focus-visible > & { + 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 { + white-space: normal; + outline: none; + + // Adjust for right-to-left languages + [dir="rtl"] & { + direction: rtl; + } + + // 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.325; + } + + // Promote children to top on focus + &:focus-within > * { + z-index: 2; + } + + // Annotation wrapper (= tooltip) + &__inner { + top: calc(var(--md-tooltip-y) + 1.2ch); + font-family: var(--md-text-font-family); + + // Annotation tooltip when not focused + :not(:focus-within) > & { + user-select: none; + pointer-events: none; + } + } + + // Annotation index + &__index { + position: relative; + z-index: 0; + margin: 0 1ch; + color: hsla(0, 0%, 100%, 1); + cursor: pointer; + transition: z-index 250ms; + user-select: none; + + // 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; + left: -0.126em; + z-index: -1; + // Hack: the first property is used as a fallback for older browsers + // which don't support the min/max/clamp math functions. + width: calc(100% + 1.2ch); + width: max(2.2ch, 100% + 1.2ch); + height: 2.2ch; + margin: 0 -0.4ch; + padding: 0 0.4ch; + background-color: var(--md-default-fg-color--lighter); + border-radius: 2ch; + transition: + color 250ms, + background-color 250ms; + content: ""; + + // [reduced motion]: Disable animation + @media not all and (prefers-reduced-motion) { + + // Annotation marker is visible + [data-md-visible] > & { + animation: pulse 2000ms infinite; + } + } + + // Annotation marker on focus/hover + :is(:focus-within, :hover) > & { + background-color: var(--md-accent-fg-color); + } + + // Annotation marker on focus + :focus-within > & { + transition: + color 250ms, + background-color 250ms; + animation: none; + } + } + + // Annotation marker + [data-md-annotation-id] { + display: inline-block; + line-height: 90%; + + // Annotation marker content + &::before { + display: inline-block; + padding-bottom: 0.1em; + vertical-align: 0.065em; + transform: scale(1.15); + transition: transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1); + content: attr(data-md-annotation-id); + + // [not print]: if we're not in print mode, show a `+` sign instead of + // the original numbers, as context is already given by the position. + @media not print { + content: "+"; + + // Annotation marker content on focus + :focus-within > & { + transform: scale(1.25) rotate(45deg); + } + } + } + } + + // Annotation index on focus/hover + :is(:focus-within, :hover) > & { + color: var(--md-accent-bg-color); + } + + // Annotation index on focus + :focus-within > & { + transition: none; + animation: none; + } + } +} diff --git a/src/assets/stylesheets/main/layout/_top.scss b/src/assets/stylesheets/main/layout/_top.scss @@ -0,0 +1,82 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + margin-inline-start: 50%; + padding: px2rem(8px) px2rem(16px); + color: var(--md-default-fg-color--light); + font-size: px2rem(14px); + background-color: var(--md-default-bg-color); + border-radius: px2rem(32px); + outline: none; + box-shadow: var(--md-shadow-z2); + transform: translate(-50%, 0); + transition: + color 125ms, + background-color 125ms, + transform 125ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 125ms; + + // [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] { + transform: translate(-50%, px2rem(4px)); + opacity: 0; + transition-duration: 0ms; + pointer-events: none; + + // 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/assets/stylesheets/main/layout/_version.scss b/src/assets/stylesheets/main/layout/_version.scss @@ -0,0 +1,149 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + outline: none; + cursor: pointer; + + // Version selection icon + &::after { + display: inline-block; + width: px2rem(8px); + height: px2rem(12px); + margin-inline-start: px2rem(8px); + background-color: currentcolor; + mask-image: var(--md-version-icon); + mask-repeat: no-repeat; + content: ""; + } + } + + // Version selection list + &__list { + position: absolute; + top: px2rem(3px); + z-index: 3; + max-height: 0; + margin: px2rem(4px) px2rem(16px); + padding: 0; + 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) { + + // 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; + outline: none; + cursor: pointer; + 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/assets/stylesheets/palette.scss b/src/assets/stylesheets/palette.scss @@ -0,0 +1,40 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/assets/stylesheets/palette/_accent.scss b/src/assets/stylesheets/palette/_accent.scss @@ -0,0 +1,61 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/assets/stylesheets/palette/_primary.scss b/src/assets/stylesheets/palette/_primary.scss @@ -0,0 +1,193 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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(0, 0%, 100%, 1); + --md-primary-fg-color--light: hsla(0, 0%, 100%, 0.7); + --md-primary-fg-color--dark: hsla(0, 0%, 0%, 0.07); + --md-primary-bg-color: hsla(0, 0%, 0%, 0.87); + --md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54); + + // Typeset `a` color shades + --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); + + // [tablet portrait +]: Header-embedded search + @include break-from-device(tablet landscape) { + + // Search form + .md-search__form { + background-color: hsla(0, 0%, 0%, 0.07); + + // Search form on hover + &:hover { + background-color: hsla(0, 0%, 0%, 0.32); + } + } + + // Search icon + .md-search__input + .md-search__icon { + color: hsla(0, 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(0, 0%, 0%, 1); + --md-primary-fg-color--light: hsla(0, 0%, 0%, 0.54); + --md-primary-fg-color--dark: hsla(0, 0%, 0%, 1); + --md-primary-bg-color: hsla(0, 0%, 100%, 1); + --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7); + + // Typeset `a` color shades + --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); + + // Header + .md-header { + background-color: hsla(0, 0%, 0%, 1); + } + + // [tablet portrait -]: Layered navigation + @include break-to-device(tablet portrait) { + + // Repository information container + .md-nav__source { + background-color: hsla(0, 0%, 0%, 0.87); + } + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + + // Search form + .md-search__form { + background-color: hsla(0, 0%, 100%, 0.12); + + // Search form on hover + &:hover { + background-color: hsla(0, 0%, 100%, 0.3); + } + } + } + + // [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(0, 0%, 0%, 1); + } + } + + // [screen +]: Set background color for tabs + @include break-from-device(screen) { + + // Navigation tabs + .md-tabs { + background-color: hsla(0, 0%, 0%, 1); + } + } +} diff --git a/src/assets/stylesheets/palette/_scheme.scss b/src/assets/stylesheets/palette/_scheme.scss @@ -0,0 +1,152 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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"] { + + // Slate's 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. This is a slate- + // specific variable, but the same approach may be adapted to custom themes. + --md-hue: 232; + + // Default color shades + --md-default-fg-color: hsla(var(--md-hue), 75%, 95%, 1); + --md-default-fg-color--light: hsla(var(--md-hue), 75%, 90%, 0.62); + --md-default-fg-color--lighter: hsla(var(--md-hue), 75%, 90%, 0.32); + --md-default-fg-color--lightest: hsla(var(--md-hue), 75%, 90%, 0.12); + --md-default-bg-color: hsla(var(--md-hue), 15%, 21%, 1); + --md-default-bg-color--light: hsla(var(--md-hue), 15%, 21%, 0.54); + --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 21%, 0.26); + --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 21%, 0.07); + + // Code color shades + --md-code-fg-color: hsla(var(--md-hue), 18%, 86%, 1); + --md-code-bg-color: hsla(var(--md-hue), 15%, 15%, 1); + + // Code highlighting color shades + --md-code-hl-color: 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 `mark` color shades + --md-typeset-mark-color: hsla(#{hex2hsl($clr-blue-a200)}, 0.3); + + // Typeset `kbd` color shades + --md-typeset-kbd-color: hsla(var(--md-hue), 15%, 94%, 0.12); + --md-typeset-kbd-accent-color: hsla(var(--md-hue), 15%, 94%, 0.2); + --md-typeset-kbd-border-color: hsla(var(--md-hue), 15%, 14%, 1); + + // Typeset `table` color shades + --md-typeset-table-color: hsla(var(--md-hue), 75%, 95%, 0.12); + + // 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%, 12%, 0.87); + --md-footer-bg-color--dark: hsla(var(--md-hue), 15%, 10%, 1); + + // Shadow depth 1 + --md-shadow-z1: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.2), + 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.3), + 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; + } + + // Show images for dark mode + img[src$="#only-dark"], + img[src$="#gh-dark-mode-only"] { + display: initial; + } + } + + // -------------------------------------------------------------------------- + + // Adjust link colors for dark primary colors + @each $name, $color in ( + "pink": hsl(340, 81%, 63%), + "purple": hsl(291, 43%, 63%), + "deep-purple": hsl(262, 63%, 70%), + "indigo": hsl(219, 56%, 63%), + "teal": hsl(174, 100%, 40%), + "green": hsl(122, 39%, 60%), + "deep-orange": hsl(14, 100%, 73%), + "brown": hsl(16, 45%, 60%), + + // Set neutral colors to indigo + "grey": hsl(219, 56%, 63%), + "blue-grey": hsl(219, 56%, 63%), + "white": hsl(219, 56%, 63%), + "black": hsl(219, 56%, 63%) + ) { + [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/assets/stylesheets/utilities/_break.scss b/src/assets/stylesheets/utilities/_break.scss @@ -0,0 +1,219 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/assets/stylesheets/utilities/_convert.scss b/src/assets/stylesheets/utilities/_convert.scss @@ -0,0 +1,79 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/base.html b/src/base.html @@ -0,0 +1,412 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 %} + + <!-- 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 }}" + /> + + <!-- Theme-color meta tag for Android --> + {% if palette.primary %} + {% import "partials/palette.html" as map %} + {% set primary = map.primary( + palette.primary | replace(" ", "-") | lower + ) %} + <meta name="theme-color" content="{{ primary }}" /> + {% endif %} + {% endif %} + + <!-- Custom icons --> + {% include "partials/icons.html" %} + {% endblock %} + + <!-- JavaScript libraries --> + {% block libs %}{% endblock %} + + <!-- Webfonts --> + {% block fonts %} + + <!-- Load fonts from Google --> + {% if config.theme.font != false %} + {% set text = config.theme.font.text | d("Roboto", true) %} + {% set code = config.theme.font.code | d("Roboto Mono", true) %} + <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 %} + + <!-- 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 | replace(" ", "-") | lower %} + {% set primary = palette.primary | replace(" ", "-") | lower %} + {% set accent = palette.accent | replace(" ", "-") | lower %} + <body + dir="{{ direction }}" + data-md-color-scheme="{{ scheme }}" + data-md-color-primary="{{ primary }}" + data-md-color-accent="{{ accent }}" + > + {% 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('skip.link.title') }} + </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') }}" + > + {% include ".icons/material/close.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-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 not "navigation.tabs.sticky" 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 not "toc.integrate" 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 --> + <div class="md-content" data-md-component="content"> + <article class="md-content__inner md-typeset"> + {% block content %} + {% include "partials/content.html" %} + {% endblock %} + </article> + + <!-- User preference: content --> + {% include "partials/javascripts/content.html" %} + </div> + </div> + + <!-- Back-to-top button --> + {% if "navigation.top" in features %} + <a + href="#" + class="md-top md-icon" + data-md-component="top" + hidden + > + {% include ".icons/material/arrow-up.svg" %} + {{ lang.t('top.title') }} + </a> + {% 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> + + <!-- 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 -%} + + <!-- Translations --> + {%- set translations = app.translations -%} + {%- for key in [ + "clipboard.copy", + "clipboard.copied", + "search.config.lang", + "search.config.pipeline", + "search.config.separator", + "search.placeholder", + "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.title" + ] -%} + {%- 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 path in config["extra_javascript"] %} + <script src="{{ path | url }}"></script> + {% endfor %} + {% endblock %} + </body> +</html> diff --git a/src/main.html b/src/main.html @@ -0,0 +1,23 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/mkdocs_theme.yml b/src/mkdocs_theme.yml @@ -0,0 +1,68 @@ +# Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION 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: [] + +# Sets the primary and accent color palettes as defined in the Material Design +# documentation - possible values can be looked up in the getting started guide +palette: + + # Primary color used for header, sidebar and links, default: indigo + primary: + + # Accent color for highlighting user interaction, default: indigo + accent: + +# 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. Some +# icons that are part of the HTML can be configured and replaced +icon: + +# Favicon to be rendered +favicon: assets/images/favicon.png + +# Material includes the search in the header as a partial, not as a separate +# template, so it's correct that search.html is missing +include_search_page: false + +# Material doesn't use MkDocs search functionality but provides its own. For +# this reason, only the search index needs to be built +search_index_only: true + +# Static pages to build +static_templates: + - 404.html diff --git a/src/overrides/assets/javascripts/bundle.ts b/src/overrides/assets/javascripts/bundle.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/components/_/index.ts b/src/overrides/assets/javascripts/components/_/index.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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("overrides/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 @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/integrations/analytics/index.ts b/src/overrides/assets/javascripts/integrations/analytics/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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" + class="mdx-sponsorship__item mdx-sponsorship__item--private" + > + +{count} + </a> + ) +} diff --git a/src/overrides/assets/stylesheets/main.scss b/src/overrides/assets/stylesheets/main.scss @@ -0,0 +1,46 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/typeset"; + +@import "main/layout/banner"; +@import "main/layout/hero"; +@import "main/layout/iconsearch"; +@import "main/layout/sponsorship"; + +@import "main/shame"; diff --git a/src/overrides/assets/stylesheets/main/_shame.scss b/src/overrides/assets/stylesheets/main/_shame.scss @@ -0,0 +1,25 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Nothing to see here, move along +// ---------------------------------------------------------------------------- diff --git a/src/overrides/assets/stylesheets/main/_typeset.scss b/src/overrides/assets/stylesheets/main/_typeset.scss @@ -0,0 +1,165 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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; + } + + // 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.) + .mdx-insiders { + color: $clr-pink-500; + } + + // Switch buttons + .mdx-switch button { + cursor: pointer; + transition: opacity 250ms; + + // Button on focus/hover + &:focus, + &:hover { + opacity: 0.75; + } + + // Code block + > code { + display: block; + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + } + } + + // Deprecation + .mdx-deprecated { + opacity: 0.5; + transition: opacity 250ms; + + // Deprecation on focus/hover + &:focus-within, + &:hover { + opacity: 1; + } + } + + // 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; + } + } + + // Blog author + .mdx-author { + display: flex; + font-size: px2rem(13.6px); + + // Blog author image + img { + height: px2rem(40px); + border-radius: 100%; + } + + // Blog author content + p { + + // TODO: refactor, use dedicated classes + &:first-child { + flex-shrink: 0; + margin-right: px2rem(16px); + } + + // Blog metadata + > span { + display: block; + } + } + } +} diff --git a/src/overrides/assets/stylesheets/main/layout/_banner.scss b/src/overrides/assets/stylesheets/main/layout/_banner.scss @@ -0,0 +1,46 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 { + + // Text link, also on focus/hover + a, + a:focus, + a:hover { + color: currentcolor; + } + + // Don't wrap name of blog article + strong { + white-space: nowrap; + } + + // Twitter icon + .twitter { + margin-inline-start: 0.2em; + } +} diff --git a/src/overrides/assets/stylesheets/main/layout/_hero.scss b/src/overrides/assets/stylesheets/main/layout/_hero.scss @@ -0,0 +1,124 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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(232, 15%, 21%, 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% + ); + } +} + +// Landing page hero +.mdx-hero { + margin: 0 px2rem(16px); + color: var(--md-primary-bg-color); + + // Hero headline + h1 { + margin-bottom: px2rem(20px); + color: currentcolor; + font-weight: 700; + + // [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); + margin-top: px2rem(70px); + padding-bottom: 14vw; + } + + // 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 + &: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/main/layout/_iconsearch.scss b/src/overrides/assets/stylesheets/main/layout/_iconsearch.scss @@ -0,0 +1,137 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 + &: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); + color: var(--md-default-fg-color--lighter); + font-size: px2rem(12.8px); + } + + // Icon search result list + &__list { + margin: 0; + // Hack: necessary because of increased specificity due to the PostCSS + // plugin which prefixes this with `[dir=...]` selectors. + margin-inline-start: 0; + padding: 0; + list-style: none; + } + + // Icon search result item + &__item { + margin: 0; + // Hack: necessary because of increased specificity due to the PostCSS + // plugin which prefixes this with `[dir=...]` selectors. + margin-inline-start: 0; + padding: px2rem(4px) px2rem(12px); + 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/main/layout/_sponsorship.scss b/src/overrides/assets/stylesheets/main/layout/_sponsorship.scss @@ -0,0 +1,129 @@ +//// +/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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: left; + width: px2rem(32px); + height: px2rem(32px); + margin: px2rem(4px); + overflow: hidden; + border-radius: 100%; + transform: scale(1); + transition: + color 125ms, + transform 125ms; + + // Sponsor item on focus/hover + &:focus, + &:hover { + transform: scale(1.1); + + // Sponsor avatar + img { + filter: grayscale(0%); + } + } + + // Private sponsor + &--private { + color: var(--md-default-fg-color--lighter); + font-weight: 700; + font-size: px2rem(12px); + line-height: px2rem(32px); + 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/blog.html b/src/overrides/blog.html @@ -0,0 +1,76 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 "overrides/main.html" %} + +<!-- Content --> +{% block content %} + {{ super() }} + + <!-- Giscus - generated by https://giscus.app --> + <h2 id="__comments">{{ lang.t("meta.comments") }}</h2> + <script + src="https://giscus.app/client.js" + data-repo="squidfunk/mkdocs-material" + data-repo-id="MDEwOlJlcG9zaXRvcnk1MDYxNzQyOA==" + data-category="_" + data-category-id="DIC_kwDOAwRcVM4CAtJY" + data-mapping="pathname" + data-reactions-enabled="1" + data-emit-metadata="1" + data-theme="light" + data-lang="en" + crossorigin="anonymous" + async + > + </script> + + <!-- Synchronize Giscus theme with palette --> + <script> + var giscus = document.querySelector("script[src*=giscus]") + + /* Set palette on initial load */ + var palette = __md_get("__palette") + if (palette && typeof palette.color === "object") { + var theme = palette.color.scheme === "slate" ? "dark" : "light" + giscus.setAttribute("data-theme", theme) + } + + /* Register event handlers after documented loaded */ + document.addEventListener("DOMContentLoaded", function() { + var ref = document.querySelector("[data-md-component=palette]") + ref.addEventListener("change", function() { + var palette = __md_get("__palette") + if (palette && typeof palette.color === "object") { + var theme = palette.color.scheme === "slate" ? "dark" : "light" + + /* Instruct Giscus to change theme */ + var frame = document.querySelector(".giscus-frame") + frame.contentWindow.postMessage( + { giscus: { setConfig: { theme } } }, + "https://giscus.app" + ) + } + }) + }) + </script> +{% endblock %} diff --git a/src/overrides/home.html b/src/overrides/home.html @@ -0,0 +1,106 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 "overrides/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/main.html b/src/overrides/main.html @@ -0,0 +1,57 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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="{{ 'overrides/assets/stylesheets/main.css' | url }}" + /> +{% endblock %} + +<!-- Announcement bar --> +{% block announce %} + <a href="https://twitter.com/teamseshisma"> + For updates follow <strong>@teamseshisma</strong> on + <span class="twemoji twitter"> + {% include ".icons/fontawesome/brands/twitter.svg" %} + </span> + <strong>Twitter</strong> + </a> +{% endblock %} + +<!-- Content --> +{% block content %} + {% include "overrides/partials/content.html" %} +{% endblock %} + +<!-- Theme-related JavaScript --> +{% block scripts %} + {{ super() }} + + <!-- Extra JavaScript (can't be set in mkdocs.yml due to content hash) --> + <script src="{{ 'overrides/assets/javascripts/bundle.js' | url }}"></script> +{% endblock %} diff --git a/src/overrides/partials/content.html b/src/overrides/partials/content.html @@ -0,0 +1,68 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +--> + +<!-- Edit and view button --> +{% if page.edit_url %} + {% set edit = "https://github.com/squidfunk/mkdocs-material/edit" %} + {% set view = "https://raw.githubusercontent.com/squidfunk/mkdocs-material" %} + <a + href="{{ page.edit_url }}" + title="{{ lang.t('edit.link.title') }}" + class="md-content__button md-icon" + > + {% include ".icons/material/file-edit-outline.svg" %} + </a> + <a + href="{{ page.edit_url | replace(edit, view) }}" + title="View source of this page" + class="md-content__button md-icon" + > + {% include ".icons/material/file-eye-outline.svg" %} + </a> +{% endif %} + +<!-- Tags --> +{% if "tags" in config.plugins %} + {% include "partials/tags.html" %} +{% endif %} + +<!-- + 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 not "\x3ch1" in page.content %} + <h1>{{ page.title | d(config.site_name, true)}}</h1> +{% endif %} + +<!-- Markdown 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" %} diff --git a/src/partials/consent.html b/src/partials/consent.html @@ -0,0 +1,91 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 %} + +<!-- Determine cookies (default to analytics, if present) --> +{% set cookies = config.extra.consent.cookies %} +{% if config.extra.analytics and not cookies %} + {% set cookies = { "analytics": "Google Analytics" } %} +{% endif %} + +<!-- Determine actions --> +{% set actions = config.extra.consent.actions %} +{% if not actions %} + {% set actions = ["accept", "manage"] %} +{% endif %} + +<!-- Consent title --> +<h4>{{ config.extra.consent.title }}</h4> +<p>{{ config.extra.consent.description }}</p> + +<!-- Consent settings --> +<input type="checkbox" class="md-toggle" id="__settings" /> +<div class="md-consent__settings"> + <ul class="task-list"> + {% for type in cookies %} + {% 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/partials/content.html b/src/partials/content.html @@ -0,0 +1,59 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +--> + +<!-- Edit button --> +{% if page.edit_url %} + <a + href="{{ page.edit_url }}" + title="{{ lang.t('edit.link.title') }}" + class="md-content__button md-icon" + > + {% include ".icons/material/pencil.svg" %} + </a> +{% endif %} + +<!-- Tags --> +{% if "tags" in config.plugins %} + {% include "partials/tags.html" %} +{% endif %} + +<!-- + 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 not "\x3ch1" in page.content %} + <h1>{{ page.title | d(config.site_name, true)}}</h1> +{% endif %} + +<!-- Markdown 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" %} diff --git a/src/partials/copyright.html b/src/partials/copyright.html @@ -0,0 +1,33 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 for Sound-Engineers around the world + {% endif %} +</div> diff --git a/src/partials/feedback.html b/src/partials/feedback.html @@ -0,0 +1,85 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 and 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 and page.meta and page.meta.title %} + {% set title = page.meta.title | urlencode %} + {% else %} + {% set title = page.title | urlencode %} + {% endif %} + + <!-- Legacy, deprecated, removed in next major version --> + {% if "{}" in rating.note %} + {{ rating.note.format(url, title) }} + + <!-- Replace {url} and {title} placeholders in note --> + {% else %} + {{ rating.note.format(url = url, title = title) }} + {% endif %} + </div> + {% endfor %} + </div> + </div> + </fieldset> + </form> +{% endif %} diff --git a/src/partials/footer.html b/src/partials/footer.html @@ -0,0 +1,96 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 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.title') }}" + {{ 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 }}" + rel="prev" + > + <div class="md-footer__button md-icon"> + {% include ".icons/material/arrow-left.svg" %} + </div> + <div class="md-footer__title"> + <div class="md-ellipsis"> + <span class="md-footer__direction"> + {{ direction }} + </span> + {{ 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 }}" + rel="next" + > + <div class="md-footer__title"> + <div class="md-ellipsis"> + <span class="md-footer__direction"> + {{ direction }} + </span> + {{ page.next_page.title }} + </div> + </div> + <div class="md-footer__button md-icon"> + {% include ".icons/material/arrow-right.svg" %} + </div> + </a> + {% endif %} + </nav> + {% 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/partials/header.html b/src/partials/header.html @@ -0,0 +1,161 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 class according to configuration --> +{% set class = "md-header" %} +{% if "navigation.tabs.sticky" in features %} + {% set class = class ~ " md-header--lifted" %} +{% endif %} + +<!-- Header --> +<header class="{{ class }}" data-md-component="header"> + <nav + class="md-header__inner md-grid" + aria-label="{{ lang.t('header.title') }}" + > + + <!-- 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"> + {% include ".icons/material/menu" ~ ".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 --> + {% if not config.theme.palette is mapping %} + <form class="md-header__option" data-md-component="palette"> + {% for option in config.theme.palette %} + {% set primary = option.primary | replace(" ", "-") | lower %} + {% set accent = option.accent | replace(" ", "-") | lower %} + <input + class="md-option" + data-md-color-media="{{ option.media }}" + data-md-color-scheme="{{ option.scheme }}" + data-md-color-primary="{{ primary }}" + data-md-color-accent="{{ accent }}" + {% 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> + {% endif %} + + <!-- Site language selector --> + {% if config.extra.alternate %} + <div class="md-header__option"></form> + <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.title') }}" + > + {% 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> + {% endif %} + + <!-- Button to open search modal --> + {% if "search" in config["plugins"] %} + <label class="md-header__button md-icon" for="__search"> + {% include ".icons/material/magnify.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/partials/icons.html b/src/partials/icons.html @@ -0,0 +1,37 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 %} diff --git a/src/partials/integrations/analytics.html b/src/partials/integrations/analytics.html @@ -0,0 +1,49 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/partials/integrations/analytics/google.html b/src/partials/integrations/analytics/google.html @@ -0,0 +1,168 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 %} + +<!-- Google Analytics 4 (G-XXXXXXXXXX) --> +{% if property.startswith("G-") %} + <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?" */ + if (document.forms.feedback) { + var feedback = document.forms.feedback + 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 */ + if (typeof location$ !== "undefined") + 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> + +<!-- Universal Analytics (UA-XXXXXXXX-X) --> +{% elif property.startswith("UA-") %} + <script id="__analytics"> + function __md_analytics() { + window.ga = window.ga || function() { + (ga.q = ga.q || []).push(arguments) + } + ga.l = +new Date() + + /* Set up integration and send page view */ + ga("create", "{{ property }}", "auto") + ga("set", "anonymizeIp", true) + ga("send", "pageview") + + /* 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) { + var page = document.location.pathname; + ga("send", "pageview", page + "?q=" + this.value) + } + }) + } + + /* Set up feedback, i.e. "Was this page helpful?" */ + if (document.forms.feedback) { + var feedback = document.forms.feedback + 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") + ga("send", "event", "feedback", "click", 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 */ + if (typeof location$ !== "undefined") + location$.subscribe(function(url) { + ga("send", "pageview", url.pathname) + }) + }) + + /* Create script tag */ + var script = document.createElement("script") + script.async = true + script.src = "https://www.google-analytics.com/analytics.js" + + /* Inject script tag */ + var container = document.getElementById("__analytics") + container.insertAdjacentElement("afterEnd", script) + } + </script> +{% endif %} diff --git a/src/partials/javascripts/announce.html b/src/partials/javascripts/announce.html @@ -0,0 +1,31 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/partials/javascripts/base.html b/src/partials/javascripts/base.html @@ -0,0 +1,48 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/partials/javascripts/consent.html b/src/partials/javascripts/consent.html @@ -0,0 +1,62 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 */ + console.log(new FormData(form)) + __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/partials/javascripts/content.html b/src/partials/javascripts/content.html @@ -0,0 +1,39 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/partials/javascripts/outdated.html b/src/partials/javascripts/outdated.html @@ -0,0 +1,29 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/partials/javascripts/palette.html b/src/partials/javascripts/palette.html @@ -0,0 +1,29 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/partials/language.html b/src/partials/language.html @@ -0,0 +1,28 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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) }}{% endmacro %} diff --git a/src/partials/languages/af.html b/src/partials/languages/af.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopieer na knipbord", + "clipboard.copied": "gekopieer na knipbord", + "edit.link.title": "Wysig hierdie bladsy", + "footer.previous": "Vorige", + "footer.next": "Volgende", + "meta.comments": "Kommentaar", + "meta.source": "Bron", + "search.config.lang": "nl", + "search.placeholder": "Soek", + "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", + "skip.link.title": "Slaan oor na inhoud", + "source.link.title": "Slaan oor na inhoud", + "source.file.date.updated": "Laaste opdatering", + "source.file.date.created": "Geskep", + "toc.title": "Inhoudsopgawe" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/ar.html b/src/partials/languages/ar.html @@ -0,0 +1,45 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "نسخ إلى الحافظة", + "clipboard.copied": "تم النسخ الى الحافظة", + "edit.link.title": "عدل الصفحة", + "footer.previous": "السابقة", + "footer.next": "التالية", + "meta.comments": "التعليقات", + "meta.source": "المصدر", + "search.config.pipeline": " ", + "search.placeholder": "بحث", + "search.result.placeholder": "اكتب لبدء البحث", + "search.result.none": "لا توجد نتائج", + "search.result.one": "نتائج البحث مستند واحد", + "search.result.other": "نتائج البحث # مستندات", + "skip.link.title": "انتقل إلى المحتوى", + "source.link.title": "اذهب إلى المصدر", + "source.file.date.updated": "اخر تحديث", + "source.file.date.created": "خلقت", + "toc.title": "جدول المحتويات" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/bg.html b/src/partials/languages/bg.html @@ -0,0 +1,51 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Копирай", + "clipboard.copied": "Копирано", + "edit.link.title": "Редактирай тази страница", + "footer.previous": "Предишна", + "footer.next": "Следваща", + "footer.title": "Долен колонтитул", + "header.title": "Горен колонтитул", + "meta.comments": "Коментари", + "meta.source": "Код", + "nav.title": "Навигация", + "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": "още # на тази страница", + "skip.link.title": "Към съдържанието", + "source.link.title": "Към хранилището", + "source.file.date.updated": "Последна промяна", + "source.file.date.created": "Създаден", + "tabs.title": "Табове", + "toc.title": "Съдържание" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/bn.html b/src/partials/languages/bn.html @@ -0,0 +1,49 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "ক্লিপবোর্ডে কপি করুন", + "clipboard.copied": "ক্লিপবোর্ডে কপি হয়েছে", + "edit.link.title": "এই পেজ এডিট করুন", + "footer.previous": "পূর্ববর্তী", + "footer.next": "পরে", + "footer.title": "ফুটার", + "header.title": "হেডার", + "meta.comments": "কমেন্ট", + "meta.source": "সোর্স", + "nav.title": "ন্যাভিগেশন", + "search.config.pipeline": " ", + "search.placeholder": "সার্চ", + "search.reset": "মুছে ফেলুন", + "search.result.placeholder": "সার্চ টাইপ করুন", + "search.result.none": "কিছু পাওয়া যায়নি", + "search.result.one": "১ টা ডকুমেন্ট", + "search.result.other": "# টা ডকুমেন্ট", + "skip.link.title": "কনটেন্টে যান", + "source.link.title": "রিপোজিটরিতে যান", + "source.file.date.updated": "শেষ আপডেট", + "source.file.date.created": "তৈরি হয়েছে", + "tabs.title": "ট্যাব", + "toc.title": "টেবিল অফ কনটেন্ট" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/ca.html b/src/partials/languages/ca.html @@ -0,0 +1,43 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Còpia al porta-retalls", + "clipboard.copied": "Copiat al porta-retalls", + "edit.link.title": "Edita aquesta pàgina", + "footer.previous": "Anterior", + "footer.next": "Següent", + "meta.comments": "Comentaris", + "meta.source": "Codi font", + "search.placeholder": "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", + "skip.link.title": "Salta el contingut", + "source.link.title": "Ves al repositori", + "source.file.date.updated": "Darrera actualització", + "source.file.date.created": "Creada", + "toc.title": "Taula de continguts" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/cs.html b/src/partials/languages/cs.html @@ -0,0 +1,43 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopírovat do schránky", + "clipboard.copied": "Zkopírováno do schránky", + "edit.link.title": "Upravit tuto stránku", + "footer.previous": "Předchozí", + "footer.next": "Další", + "meta.comments": "Komentáře", + "meta.source": "Zdroj", + "search.placeholder": "Hledat", + "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: #", + "skip.link.title": "Přeskočit obsah", + "source.link.title": "Přejít do repozitáře", + "source.file.date.updated": "Poslední aktualizace", + "source.file.date.created": "Vytvořeno", + "toc.title": "Obsah" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/da.html b/src/partials/languages/da.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopiér til udklipsholderen", + "clipboard.copied": "Kopieret til udklipsholderen", + "edit.link.title": "Redigér denne side", + "footer.previous": "Forrige", + "footer.next": "Næste", + "meta.comments": "Kommentarer", + "meta.source": "Kilde", + "search.config.lang": "da", + "search.placeholder": "Søg", + "search.result.placeholder": "Indtast søgeord", + "search.result.none": "Ingen resultater fundet", + "search.result.one": "1 resultat", + "search.result.other": "# resultater", + "skip.link.title": "Gå til indholdet", + "source.link.title": "Åbn arkiv", + "source.file.date.updated": "Sidste ændring", + "source.file.date.created": "Oprettet", + "toc.title": "Indholdsfortegnelse" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/de.html b/src/partials/languages/de.html @@ -0,0 +1,58 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "announce.dismiss": "Nicht mehr anzeigen", + "clipboard.copy": "In Zwischenablage kopieren", + "clipboard.copied": "In Zwischenablage kopiert", + "consent.accept": "Akzeptieren", + "consent.manage": "Einstellungen", + "consent.reject": "Ablehnen", + "edit.link.title": "Seite editieren", + "footer.previous": "Zurück", + "footer.next": "Weiter", + "meta.comments": "Kommentare", + "meta.source": "Quellcode", + "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", + "search.title": "Suche", + "select.language.title": "Sprache wechseln", + "select.version.title": "Version auswählen", + "skip.link.title": "Zum Inhalt", + "source.link.title": "Quellcode", + "source.file.date.updated": "Letztes Update", + "source.file.date.created": "Erstellt", + "toc.title": "Inhaltsverzeichnis", + "top.title": "Zurück zum Seitenanfang" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/el.html b/src/partials/languages/el.html @@ -0,0 +1,58 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Αντιγραφή στο πρόχειρο", + "clipboard.copied": "Αντιγράφηκε στο πρόχειρο", + "edit.link.title": "Επεξεργασία αυτής της σελίδας", + "footer.previous": "Προηγούμενο", + "footer.next": "Επόμενο", + "footer.title": "Υποσέλιδο", + "header.title": "Κεφαλίδα", + "meta.comments": "Σχόλια", + "meta.source": "Πηγή", + "nav.title": "Πλοήγηση", + "search.config.pipeline": "stopWordFilter", + "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": "Λείπει", + "search.title": "Αναζήτηση", + "select.language.title": "Επιλογή γλώσσας", + "select.version.title": "Επιλογή έκδοσης", + "skip.link.title": "Μετάβαση στο περιεχόμενο", + "source.link.title": "Μετάβαση στο αποθετήριο", + "source.file.date.updated": "τελευταία ενημέρωση", + "source.file.date.created": "Δημιουργήθηκε", + "tabs.title": "Καρτέλες", + "toc.title": "Πίνακας περιεχομένων", + "top.title": "Επιστροφή στην αρχή" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/en.html b/src/partials/languages/en.html @@ -0,0 +1,65 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "announce.dismiss": "Don't show this again", + "clipboard.copy": "Copy to clipboard", + "clipboard.copied": "Copied to clipboard", + "consent.accept": "Accept", + "consent.manage": "Manage settings", + "consent.reject": "Reject", + "edit.link.title": "Edit this page", + "footer.previous": "Previous", + "footer.next": "Next", + "footer.title": "Footer", + "header.title": "Header", + "meta.comments": "Comments", + "meta.source": "Source", + "nav.title": "Navigation", + "search.config.lang": "en", + "search.config.pipeline": "trimmer, 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", + "search.title": "Search", + "select.language.title": "Select language", + "select.version.title": "Select version", + "skip.link.title": "Skip to content", + "source.link.title": "Go to repository", + "source.file.date.updated": "Last update", + "source.file.date.created": "Created", + "tabs.title": "Tabs", + "toc.title": "Table of contents", + "top.title": "Back to top" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/eo.html b/src/partials/languages/eo.html @@ -0,0 +1,49 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopii al tondujo", + "clipboard.copied": "Kopiado al klipo", + "edit.link.title": "Redakti ĉi tiun paĝon", + "footer.previous": "Antaŭa", + "footer.next": "Sekva", + "footer.title": "Piedlinio", + "header.title": "Kaplinio", + "meta.comments": "Komentoj", + "meta.source": "Fontkodo", + "nav.title": "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", + "skip.link.title": "Saltu al enhavo", + "source.link.title": "Iru al deponejo", + "source.file.date.updated": "Lasta ĝisdatigo", + "source.file.date.created": "Kreita", + "tabs.title": "Langetoj", + "toc.title": "Enhavtabelo" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/es.html b/src/partials/languages/es.html @@ -0,0 +1,58 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Copiar al portapapeles", + "clipboard.copied": "Copiado al portapapeles", + "consent.accept": "Aceptar", + "consent.manage": "Gestionar cookies", + "edit.link.title": "Editar esta página", + "footer.previous": "Anterior", + "footer.next": "Siguiente", + "footer.title": "Pie", + "header.title": "Cabecera", + "meta.comments": "Comentarios", + "meta.source": "Fuente", + "nav.title": "Navegación", + "search.config.lang": "es", + "search.placeholder": "Búsqueda", + "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.title": "Seleccionar idioma", + "select.version.title": "Seleccionar versión", + "skip.link.title": "Saltar a contenido", + "source.link.title": "Ir al repositorio", + "source.file.date.updated": "Última actualización", + "source.file.date.created": "Creado", + "tabs.title": "Pestañas", + "toc.title": "Tabla de contenidos", + "top.title": "Volver al principio" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/et.html b/src/partials/languages/et.html @@ -0,0 +1,43 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopeeri lõikelauale", + "clipboard.copied": "Kopeeritud", + "edit.link.title": "Muuda seda lehte", + "footer.previous": "Eelmine", + "footer.next": "Järgmine", + "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", + "skip.link.title": "Keri sisuni", + "source.link.title": "Ava repositooriumis", + "source.file.date.updated": "Viimane uuendus", + "source.file.date.created": "Loodud", + "toc.title": "Sisukord" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/fa.html b/src/partials/languages/fa.html @@ -0,0 +1,45 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "کپی کردن", + "clipboard.copied": "کپی شد", + "edit.link.title": "این صفحه را ویرایش کنید", + "footer.previous": "قبلی", + "footer.next": "بعدی", + "meta.comments": "نظرات", + "meta.source": "منبع", + "search.config.pipeline": " ", + "search.placeholder": "جستجو", + "search.result.placeholder": "برای شروع جستجو تایپ کنید", + "search.result.none": "سندی یافت نشد", + "search.result.one": "1 سند یافت شد", + "search.result.other": "# سند یافت شد", + "skip.link.title": "پرش به محتویات", + "source.link.title": "رفتن به مخزن", + "source.file.date.updated": "اخرین بروزرسانی", + "source.file.date.created": "ایجاد شده", + "toc.title": "فهرست موضوعات" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/fi.html b/src/partials/languages/fi.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopioi leikepöydälle", + "clipboard.copied": "Kopioitu leikepöydälle", + "edit.link.title": "Muokkaa tätä sivua", + "footer.previous": "Edellinen", + "footer.next": "Seuraava", + "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", + "skip.link.title": "Hyppää sisältöön", + "source.link.title": "Mene repositoryyn", + "source.file.date.updated": "Viimeisin päivitys", + "source.file.date.created": "Luotu", + "toc.title": "Sisällysluettelo" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/fr.html b/src/partials/languages/fr.html @@ -0,0 +1,59 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "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", + "edit.link.title": "Editer cette page", + "footer.previous": "Précédent", + "footer.next": "Suivant", + "footer.title": "Pied de page", + "header.title": "En-tête", + "meta.comments": "Commentaires", + "meta.source": "Source", + "nav.title": "Navigation", + "search.config.lang": "fr", + "search.placeholder": "Rechercher", + "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.title": "Sélectionner la langue", + "select.version.title": "Sélectionner la version", + "skip.link.title": "Aller au contenu", + "source.link.title": "Aller au dépôt", + "source.file.date.updated": "Dernière mise à jour", + "source.file.date.created": "Créé", + "tabs.title": "Onglets", + "toc.title": "Table des matières", + "top.title": "Retour en haut de la page" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/gl.html b/src/partials/languages/gl.html @@ -0,0 +1,56 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Copiar no cortapapeis", + "clipboard.copied": "Copiado no cortapapeis", + "edit.link.title": "Editar esta páxina", + "footer.previous": "Anterior", + "footer.next": "Seguinte", + "footer.title": "Pé", + "header.title": "Cabeceira", + "meta.comments": "Comentarios", + "meta.source": "Fonte", + "nav.title": "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.title": "Seleccionar idioma", + "select.version.title": "Seleccionar version", + "skip.link.title": "Ir ao contido", + "source.link.title": "Ir ao repositorio", + "source.file.date.updated": "Última actualización", + "source.file.date.created": "Creada", + "tabs.title": "Pestanas", + "toc.title": "Táboa de contidos", + "top.title": "Volver ao principio" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/he.html b/src/partials/languages/he.html @@ -0,0 +1,63 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "announce.dismiss": "לא להציג את זה שוב", + "clipboard.copy": "העתקה ללוח", + "clipboard.copied": "הועתק ללוח", + "consent.accept": "לקבל", + "consent.manage": "לנהל הגדרות", + "consent.reject": "לדחות", + "edit.link.title": "עריכת הדף הזה", + "footer.previous": "הקודם", + "footer.next": "הבא", + "footer.title": "כותרת תחתונה", + "header.title": "כותרת עליונה", + "meta.comments": "הערות", + "meta.source": "מקור", + "nav.title": "ניווט", + "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": "חסר", + "search.title": "חיפוש", + "select.language.title": "בחירת שפה", + "select.version.title": "בחירת גרסה", + "skip.link.title": "לדלג לתוכן", + "source.link.title": "לעבור אל המאגר", + "source.file.date.updated": "עדכון אחרון", + "source.file.date.created": "נוצר", + "tabs.title": "לשוניות", + "toc.title": "תוכן העניינים", + "top.title": "חזרה למעלה" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/hi.html b/src/partials/languages/hi.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "क्लिपबोर्ड पर कॉपी करें", + "clipboard.copied": "क्लिपबोर्ड पर कॉपी कर दिया गया", + "edit.link.title": "इस पृष्ठ को संपादित करें", + "footer.previous": "पिछला", + "footer.next": "आगामी", + "meta.comments": "टिप्पणियाँ", + "meta.source": "स्रोत", + "search.config.lang": "hi", + "search.placeholder": "खोज", + "search.result.placeholder": "खोज शुरू करने के लिए टाइप करें", + "search.result.none": "कोई मिलान डॉक्यूमेंट नहीं", + "search.result.one": "1 मिलान डॉक्यूमेंट", + "search.result.other": "# मिलान डाक्यूमेंट्स", + "skip.link.title": "विषय पर बढ़ें", + "source.link.title": "रिपॉजिटरी पर जाएं", + "source.file.date.updated": "आखिरी अपडेट", + "source.file.date.created": "बनाया था", + "toc.title": "विषय - सूची" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/hr.html b/src/partials/languages/hr.html @@ -0,0 +1,61 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "announce.dismiss": "Ne prikazuj ovo opet", + "clipboard.copy": "Kopirajte u međuspremnik", + "clipboard.copied": "Kopirano u međuspremnik", + "consent.accept": "Prihvati", + "consent.manage": "Upravljaj postavkama", + "consent.reject": "Odbij", + "edit.link.title": "Uredi stranicu", + "footer.previous": "Prethodno", + "footer.next": "Sljedeće", + "footer.title": "Podnožje", + "header.title": "Zaglavlje", + "meta.comments": "Komentari", + "meta.source": "Izvor", + "nav.title": "Navigacija", + "search.placeholder": "Pretraživanje", + "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": "# rezultata pretraživanja", + "search.result.more.one": "1 rezultat na ovoj stranici", + "search.result.more.other": "# rezultata na ovoj stranici", + "search.result.term.missing": "Nedostaje", + "search.title": "Pretraživanje", + "select.language.title": "Odabir jezika", + "select.version.title": "Odabir verzije", + "skip.link.title": "Preskočite na sadržaj", + "source.link.title": "Idite u repozitorij", + "source.file.date.updated": "Zadnje ažuriranje", + "source.file.date.created": "Stvoreno", + "tabs.title": "Kartice", + "toc.title": "Sadržaj", + "top.title": "Vratite se na vrh" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/hu.html b/src/partials/languages/hu.html @@ -0,0 +1,53 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Másolás vágólapra", + "clipboard.copied": "Vágólapra másolva", + "edit.link.title": "Oldal szerkesztése", + "footer.previous": "Előző", + "footer.next": "Következő", + "footer.title": "Élőláb", + "header.title": "Élőfej", + "meta.comments": "Hozzászólások", + "meta.source": "Forrás", + "nav.title": "Navigáció", + "search.config.lang": "hu", + "search.placeholder": "Keresé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", + "skip.link.title": "Kihagyás", + "source.link.title": "Főoldalra ugrás", + "source.file.date.updated": "Utolsó frissítés", + "source.file.date.created": "Létrehozva", + "tabs.title": "Lapok", + "toc.title": "Tartalomjegyzék" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/hy.html b/src/partials/languages/hy.html @@ -0,0 +1,58 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Պատճենել", + "clipboard.copied": "Պատճենված է", + "edit.link.title": "Խմբագրել այս էջը", + "footer.previous": "Նախորդը", + "footer.next": "Հաջորդը", + "footer.title": "Վերջնագիր", + "header.title": "Գլխագիր", + "meta.comments": "Մեկնաբանությունները", + "meta.source": "Աղբյուր", + "nav.title": "Տեղորոշում", + "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": "Բացակայում է", + "search.title": "Փնտրում", + "select.language.title": "Ընտրել լեզուն", + "select.version.title": "Ընտրել տարբերակը", + "skip.link.title": "Անցնել պարունակությանը", + "source.link.title": "Դեպի պահոց", + "source.file.date.updated": "Վերջին թարմացումը", + "source.file.date.created": "Ստեղծված է", + "tabs.title": "Ներդիրներ", + "toc.title": "Բովանդակություն", + "top.title": "Վերադառնալ սկիզբ" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/id.html b/src/partials/languages/id.html @@ -0,0 +1,43 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Salin ke memori", + "clipboard.copied": "Tersalin ke memori", + "edit.link.title": "Ubah halaman ini", + "footer.previous": "Sebelumnya", + "footer.next": "Selanjutnya", + "meta.comments": "Komentar", + "meta.source": "Sumber", + "search.config.pipeline": " ", + "search.placeholder": "Cari", + "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", + "skip.link.title": "Lewati ke isi", + "source.link.title": "Menuju repositori", + "source.file.date.created": "Dibuat", + "toc.title": "Daftar isi" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/is.html b/src/partials/languages/is.html @@ -0,0 +1,50 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Afrita í klemmuspjald", + "clipboard.copied": "Afritað í klemmuspjald", + "edit.link.title": "Ritvinna þessa síðu", + "footer.previous": "Fyrra", + "footer.next": "Næsta", + "footer.title": "Síðufótur", + "header.title": "Haus", + "meta.comments": "Athugasemdir", + "meta.source": "Grunnur", + "nav.title": "Valmynd", + "search.placeholder": "Leit", + "search.reset": "Hreinsa", + "search.result.placeholder": "Sláðu inn til að hefja leit", + "search.result.none": "Engin skjöl fundust", + "search.result.one": "1 skjal fannst", + "search.result.other": "# skjöl fundust", + "search.result.more.one": "1 til viðbótar á þessari síðu", + "search.result.more.other": "# til viðbótar á þessari síðu", + "skip.link.title": "Hoppa yfir í efni", + "source.link.title": "Fara í gagnahirslu (e. repository)", + "source.file.date.updated": "Síðasta uppfærsla", + "source.file.date.created": "Búið til", + "tabs.title": "Flipar", + "toc.title": "Efnisyfirlit" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/it.html b/src/partials/languages/it.html @@ -0,0 +1,58 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Copia", + "clipboard.copied": "Copiato", + "edit.link.title": "Modifica", + "footer.previous": "Precedente", + "footer.next": "Prossimo", + "footer.title": "Piede", + "header.title": "Intestazione", + "meta.comments": "Commenti", + "meta.source": "Sorgente", + "nav.title": "Navigazione", + "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", + "search.title": "Cerca", + "select.language.title": "Seleziona la lingua", + "select.version.title": "Seleziona la versione", + "skip.link.title": "Vai al contenuto", + "source.link.title": "Apri repository", + "source.file.date.updated": "Ultimo aggiornamento", + "source.file.date.created": "Creata", + "tabs.title": "Tabs", + "toc.title": "Indice", + "top.title": "Torna su" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/ja.html b/src/partials/languages/ja.html @@ -0,0 +1,55 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "クリップボードへコピー", + "clipboard.copied": "コピーしました", + "edit.link.title": "編集", + "footer.previous": "前", + "footer.next": "次", + "footer.title": "フッター", + "header.title": "ヘッダー", + "meta.comments": "コメント", + "meta.source": "ソース", + "nav.title": "ナビゲーション", + "search.config.lang": "ja", + "search.config.pipeline": "trimmer, stemmer", + "search.config.separator": "[\\s\\- 、。,.]+", + "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": "検索に含まれない", + "skip.link.title": "コンテンツにスキップ", + "source.link.title": "リポジトリへ", + "source.file.date.updated": "最終更新日", + "source.file.date.created": "作成した", + "tabs.title": "タブ", + "toc.title": "目次" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/ka.html b/src/partials/languages/ka.html @@ -0,0 +1,49 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "კოპირება", + "clipboard.copied": "კოპირებულია", + "edit.link.title": "გვერდის რედარქირება", + "footer.previous": "წინა", + "footer.next": "შემდეგი", + "meta.comments": "კომენტარები", + "meta.source": "წყარო", + "nav.title": "ნავიგაცია", + "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": "კიდევ # ამ გვერდზე", + "skip.link.title": "კონტენტზე გადასვლა", + "source.link.title": "საცავში გადასვლა", + "source.file.date.updated": "ბოლო განახლება", + "source.file.date.created": "შეიქმნა", + "tabs.title": "ტაბები", + "toc.title": "სარჩევი" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/kr.html b/src/partials/languages/kr.html @@ -0,0 +1,54 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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": "kr", + "clipboard.copy": "클립보드로 복사", + "clipboard.copied": "클립보드에 복사됨", + "edit.link.title": "이 페이지를 편집", + "footer.previous": "이전", + "footer.next": "다음", + "meta.comments": "댓글", + "meta.source": "출처", + "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": "포함되지 않은 검색어", + "search.title": "검색", + "select.language.title": "언어설정", + "select.version.title": "버전 선택", + "skip.link.title": "콘텐츠로 이동", + "source.link.title": "저장소로 이동", + "source.file.date.updated": "마지막 업데이트", + "source.file.date.created": "작성일", + "toc.title": "목차", + "top.title": "맨위로" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/lt.html b/src/partials/languages/lt.html @@ -0,0 +1,58 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopijuoti į iškarpinę", + "clipboard.copied": "Nukopijuota į iškarpinę", + "edit.link.title": "Redaguoti šį puslapį", + "footer.previous": "Ankstesnis", + "footer.next": "Sekantis", + "footer.title": "Poraštė", + "header.title": "Antraštė", + "meta.comments": "Komentarai", + "meta.source": "Išeitinis kodas", + "nav.title": "Navigacija", + "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", + "search.title": "Paieška", + "select.language.title": "Pasirinkti kalbą", + "select.version.title": "Pasrinkti versiją", + "skip.link.title": "Pereiti prie turinio", + "source.link.title": "Eiti į saugyklą", + "source.file.date.updated": "Paskutinis atnaujinimas", + "source.file.date.created": "Sukurta", + "tabs.title": "Skirtukai", + "toc.title": "Turinys", + "top.title": "Grįžti į viršų" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/lv.html b/src/partials/languages/lv.html @@ -0,0 +1,55 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopēt starpliktuvē", + "clipboard.copied": "Kopēts starpliktuvē", + "edit.link.title": "Rediģēt šo lapu", + "footer.previous": "Iepriekšējais", + "footer.next": "Nākamais", + "footer.title": "Kājene", + "header.title": "Galvene", + "meta.comments": "Komentārs", + "meta.source": "Avots", + "nav.title": "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.title": "Izvēlies valodu", + "select.version.title": "Izvēlies versiju", + "skip.link.title": "Pāriet uz saturu", + "source.link.title": "Doties uz repozitoriju", + "source.file.date.updated": "Pēdējoreiz atjaunots", + "source.file.date.created": "Izveidots", + "tabs.title": "Cilnes", + "toc.title": "Satura rādītājs", + "top.title": "Atpakaļ uz augšu" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/mk.html b/src/partials/languages/mk.html @@ -0,0 +1,56 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Копирај во таблата", + "clipboard.copied": "Копирано", + "edit.link.title": "Уредете ја оваа страница", + "footer.previous": "Претходно", + "footer.next": "Следно", + "footer.title": "Подножје", + "header.title": "Заглавје", + "meta.comments": "Коментари", + "meta.source": "Извор", + "nav.title": "Наслов за навигација", + "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.title": "Изберете јазик", + "select.version.title": "Изберете верзија", + "skip.link.title": "Прескокнете до содржината", + "source.link.title": "Одете до складиштето", + "source.file.date.updated": "Последно ажурирање", + "source.file.date.created": "Создаден", + "tabs.title": "Јазичиња", + "toc.title": "Содржина", + "top.title": "Вратете се на почетокот" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/mn.html b/src/partials/languages/mn.html @@ -0,0 +1,51 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Хуулах", + "clipboard.copied": "Санах ойд хуулах", + "edit.link.title": "Хуудас засварлах", + "footer.previous": "Өмнөх", + "footer.next": "Дараах", + "footer.title": "Хөл", + "header.title": "Толгой", + "meta.comments": "Сэтгэгдэл", + "meta.source": "Эх үүсвэр", + "nav.title": "Чиглүүлэгч", + "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": "# илүү хуудас байна", + "skip.link.title": "Агуулгыг алгасах", + "source.link.title": "Хадгалах сан руу очих", + "source.file.date.updated": "Сүүлийн шинэчлэлт", + "source.file.date.created": "Үүсгэсэн", + "tabs.title": "Табууд", + "toc.title": "Агуулга" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/ms.html b/src/partials/languages/ms.html @@ -0,0 +1,55 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Salin ke papan keratan", + "clipboard.copied": "Disalin ke papan keratan", + "edit.link.title": "Edit halaman ini", + "footer.previous": "Sebelumnya", + "footer.next" : "Seterusnya", + "footer.title": "Pengaki", + "header.title": "Pengepala", + "meta.comments": "Komen", + "meta.source": "Sumber", + "nav.title": "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.title": "Pilih bahasa", + "select.version.title": "Pilih versi", + "skip.link.title": "Langkau tajuk talian", + "source.link.title": "tajuk talian asal", + "source.file.date.updated": "Tarikh fil dikemas kini", + "source.file.date.created": "tarikh fil asal dicipta", + "tabs.title": "Tab", + "toc.title": "Jadual kandungan", + "top.title": "Kembali ke atas" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/my.html b/src/partials/languages/my.html @@ -0,0 +1,49 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "ကလစ်ဘုတ် သို့ ကူးယူရန်", + "clipboard.copied": "ကလစ်ဘုတ် သို့ ကူယူပြီး", + "edit.link.title": "ဤ စာမျက်နှာကို ပြင်ရန်", + "footer.previous": "နောက်သို့", + "footer.next": "ရှေ့သို့", + "footer.title": "အောက်ခြေ", + "header.title": "ခေါင်းပိုင်း", + "meta.comments": "မှတ်ချက်များ", + "meta.source": "ရင်းမြစ်", + "nav.title": "လမ်းညွှန်", + "search.config.pipeline": " ", + "search.placeholder": "ရှာရန်", + "search.reset": "ရှင်းလင်း", + "search.result.placeholder": "ရှာဖွေခြင်းစရန် စာရိုက်ပါ", + "search.result.none": "တူညီသော စာရွက်စာတမ်းများ မရှိပါ", + "search.result.one": "စာရွက်စာတမ်း ၁ ခု တူညီသည်", + "search.result.other": "စာရွက်စာတမ်း # ခု တူညီသည်", + "skip.link.title": "မာတိကာ သို့ သွားရန်", + "source.link.title": "repository သို့ သွားရန်", + "source.file.date.updated": "နောက်ဆုံး ထုတ်ပြန်ချက်", + "source.file.date.created": "နေပြည်တော်", + "tabs.title": "တက်များ", + "toc.title": "ပါဝင်အကြောင်းအရာများ" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/nl.html b/src/partials/languages/nl.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopiëren naar klembord", + "clipboard.copied": "Gekopieerd naar klembord", + "edit.link.title": "Wijzig deze pagina", + "footer.previous": "Vorige", + "footer.next": "Volgende", + "meta.comments": "Reacties", + "meta.source": "Bron", + "search.config.lang": "nl", + "search.placeholder": "Zoeken", + "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", + "skip.link.title": "Ga naar inhoud", + "source.link.title": "Ga naar repository", + "source.file.date.updated": "Laatst geüpdatet", + "source.file.date.created": "Gecreëerd", + "toc.title": "Inhoudsopgave" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/nn.html b/src/partials/languages/nn.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopier til utklippstavla", + "clipboard.copied": "Kopiert til utklippstavla", + "edit.link.title": "Rediger denne sida", + "footer.previous": "Førre", + "footer.next": "Neste", + "meta.comments": "Kommentarar", + "meta.source": "Kjelde", + "search.config.lang": "no", + "search.placeholder": "Søk", + "search.result.placeholder": "Skriv søkeord", + "search.result.none": "Ingen treff", + "search.result.one": "1 treff", + "search.result.other": "# treff", + "skip.link.title": "Gå til innhald", + "source.link.title": "Gå til kjelde", + "source.file.date.updated": "Siste oppdatering", + "source.file.date.created": "Laget", + "toc.title": "Innhaldsliste" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/no.html b/src/partials/languages/no.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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": "no", + "clipboard.copy": "Kopier til utklippstavlen", + "clipboard.copied": "Kopiert til utklippstavlen", + "edit.link.title": "Rediger denne siden", + "footer.previous": "Forrige", + "footer.next": "Neste", + "meta.comments": "Kommentarer", + "meta.source": "Kilde", + "search.config.lang": "no", + "search.placeholder": "Søk", + "search.result.placeholder": "Skriv søkeord", + "search.result.none": "Ingen treff", + "search.result.one": "1 treff", + "search.result.other": "# treff", + "skip.link.title": "Gå til innhold", + "source.link.title": "Gå til kilde", + "source.file.date.updated": "Siste oppdatering", + "source.file.date.created": "Created", + "toc.title": "Innholdsfortegnelse" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/pl.html b/src/partials/languages/pl.html @@ -0,0 +1,53 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopiuj do schowka", + "clipboard.copied": "Skopiowane", + "edit.link.title": "Edytuj tę stronę", + "footer.previous": "Poprzednia strona", + "footer.next": "Następna strona", + "footer.title": "Stopka", + "header.title": "Nagłówek", + "meta.comments": "Komentarze", + "meta.source": "Kod źródłowy", + "search.config.pipeline": " ", + "nav.title": "Nawigacja", + "search.placeholder": "Szukaj", + "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", + "skip.link.title": "Przejdź do treści", + "source.link.title": "Idź do repozytorium", + "source.file.date.updated": "Ostatnia aktualizacja", + "source.file.date.created": "Utworzony", + "tabs.title": "Zakładki", + "toc.title": "Spis treści" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/pt-BR.html b/src/partials/languages/pt-BR.html @@ -0,0 +1,57 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Copiar para área de Transferência", + "clipboard.copied": "Copiado para área de Transferência", + "edit.link.title": "Editar esta página", + "footer.previous": "Anterior", + "footer.next": "Próximo", + "footer.title": "Rodapé", + "header.title": "Cabeçalho", + "meta.comments": "Comentários", + "meta.source": "Origem", + "nav.title": "Navegação", + "search.config.lang": "pt", + "search.placeholder": "Buscar", + "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": "1 more on this page", + "search.result.more.other": "# more on this page", + "search.result.term.missing": "Perdido", + "search.title": "Pesquisar", + "select.language.title": "Selecione a linguagem", + "select.version.title": "Selecione a versão", + "skip.link.title": "Pular para conteúdo", + "source.link.title": "Ir para repositório", + "source.file.date.updated": "Ultima atualização", + "source.file.date.created": "Criado em", + "tabs.title": "Abas", + "toc.title": "Indice", + "top.title": "Voltar para o topo" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/pt.html b/src/partials/languages/pt.html @@ -0,0 +1,58 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Copiar para área de transferência", + "clipboard.copied": "Copiado para área de transferência", + "edit.link.title": "Editar esta página", + "footer.previous": "Anterior", + "footer.next": "Próximo", + "footer.title": "Rodapé", + "header.title": "Cabeçalho", + "meta.comments": "Comentários", + "meta.source": "Fonte", + "nav.title": "Navegação", + "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", + "search.title": "Pesquisar", + "select.language.title": "Selecione o idioma", + "select.version.title": "Selecione a versão", + "skip.link.title": "Ir para o conteúdo", + "source.link.title": "Ir ao repositório", + "source.file.date.updated": "Última atualização", + "source.file.date.created": "Criada", + "tabs.title": "Abas", + "toc.title": "Índice", + "top.title": "Voltar ao topo" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/ro.html b/src/partials/languages/ro.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Copiază în clipboard", + "clipboard.copied": "Copiat în clipboard", + "edit.link.title": "Editeaza această pagină", + "footer.previous": "Anterior", + "footer.next": "Următor", + "meta.comments": "Comentarii", + "meta.source": "Sursă", + "search.config.lang": "ro", + "search.placeholder": "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", + "skip.link.title": "Sari la conținut", + "source.link.title": "Accesează repository-ul", + "source.file.date.updated": "Ultima actualizare", + "source.file.date.created": "Creată", + "toc.title": "Cuprins" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/ru.html b/src/partials/languages/ru.html @@ -0,0 +1,58 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Копировать в буфер", + "clipboard.copied": "Скопировано в буфер", + "edit.link.title": "Редактировать страницу", + "footer.previous": "Назад", + "footer.next": "Вперед", + "footer.title": "Нижний колонтитул", + "header.title": "Верхний колонтитул", + "meta.comments": "Комментарии", + "meta.source": "Исходный код", + "nav.title": "Навигация", + "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": "Отсутствует", + "search.title": "Поиск", + "select.language.title": "Выберите язык", + "select.version.title": "Выберите версию", + "skip.link.title": "Перейти к содержанию", + "source.link.title": "Перейти к репозиторию", + "source.file.date.updated": "Последнее обновление", + "source.file.date.created": "Дата создания", + "tabs.title": "Вкладки", + "toc.title": "Содержание раздела", + "top.title": "К началу" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/sh.html b/src/partials/languages/sh.html @@ -0,0 +1,57 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopiraj u klipbord", + "clipboard.copied": "Iskopirano u klipbord", + "edit.link.title": "Ažuriraj stranicu", + "footer.previous": "Prethodno", + "footer.next": "Sledeće", + "footer.title": "Podnožje", + "header.title": "Zaglavlje", + "meta.comments": "Komentari", + "meta.source": "Izvor", + "nav.title": "Navigacija", + "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", + "search.title": "Pretraga", + "select.language.title": "Izaberi jezik", + "select.version.title": "Izaberi verziju", + "skip.link.title": "Idi na tekst", + "source.link.title": "Idi u repozitorijum", + "source.file.date.updated": "Ažuriran", + "source.file.date.created": "Kreiran", + "tabs.title": "Tabovi", + "toc.title": "Sadržaj", + "top.title": "Nazad na vrh" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/si.html b/src/partials/languages/si.html @@ -0,0 +1,51 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "කොපි කරන්න", + "clipboard.copied": "කොපි කළා", + "edit.link.title": "පිටුව සංස්කරණය", + "footer.previous": "පසුගිය", + "footer.next": "මීළඟ", + "footer.title": "පාදම", + "header.title": "ශීර්ෂය", + "meta.comments": "ප්‍රතිචාර", + "meta.source": "මූලාශ්‍රය", + "nav.title": "යාත්‍රණය", + "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": "තව ප්‍රතිඵල # ක්", + "skip.link.title": "අන්තර්ගතය වෙත යන්න", + "source.link.title": "රිපොසිටරියට යන්න", + "source.file.date.updated": "අවසන් යාවත්කාලීන වීම", + "source.file.date.created": "ٺاھيو ويو", + "tabs.title": "ටැබ්ස්", + "toc.title": "පටුන" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/sk.html b/src/partials/languages/sk.html @@ -0,0 +1,43 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopírovať do schránky", + "clipboard.copied": "Skopírované do schránky", + "edit.link.title": "Upraviť túto stránku", + "footer.previous": "Späť", + "footer.next": "Ďalej", + "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: #", + "skip.link.title": "Preskočiť na obsah", + "source.link.title": "Zobraziť repozitár", + "source.file.date.updated": "Posledná aktualizácia", + "source.file.date.created": "Vytvorené", + "toc.title": "Obsah" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/sl.html b/src/partials/languages/sl.html @@ -0,0 +1,43 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopiraj v odložišče", + "clipboard.copied": "Kopirano v odložišče", + "edit.link.title": "Uredi stran", + "footer.previous": "Prejšnja stran", + "footer.next": "Naslednja stran", + "meta.comments": "Komentarji", + "meta.source": "Izvorna koda", + "search.placeholder": "Išči", + "search.result.placeholder": "Vpiši iskalni niz", + "search.result.none": "Ni zadetkov", + "search.result.one": "1 zadetek", + "search.result.other": "# zadetkov", + "skip.link.title": "Skoči na vsebino", + "source.link.title": "Pojdi na repozitorij", + "source.file.date.updated": "Zadnja posodobitev", + "source.file.date.created": "Ustvarjeno", + "toc.title": "Kazalo" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/sr.html b/src/partials/languages/sr.html @@ -0,0 +1,57 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Копирај у клипборд", + "clipboard.copied": "Ископирано у клипборд", + "edit.link.title": "Ажурирај страницу", + "footer.previous": "Претходно", + "footer.next": "Следеће", + "footer.title": "Подножје", + "header.title": "Заглавље", + "meta.comments": "Коментари", + "meta.source": "Извор", + "nav.title": "Навигација", + "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": "Недостаје", + "search.title": "Претрага", + "select.language.title": "Изабери језик", + "select.version.title": "Изабери верзију", + "skip.link.title": "Иди на текст", + "source.link.title": "Иди у репозиторијум", + "source.file.date.updated": "Ажуриран", + "source.file.date.created": "Креиран", + "tabs.title": "Табови", + "toc.title": "Садржај", + "top.title": "Назад на врх" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/sv.html b/src/partials/languages/sv.html @@ -0,0 +1,60 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopiera till urklipp", + "clipboard.copied": "Kopierat till urklipp", + "consent.accept": "Acceptera", + "consent.manage": "Hantera inställningar", + "edit.link.title": "Redigera sidan", + "footer.previous": "Föregående", + "footer.next": "Nästa", + "footer.title": "Sidfot", + "header.title": "Sidhuvud", + "meta.comments": "Kommentarer", + "meta.source": "Källa", + "nav.title": "Navigation", + "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 sidan", + "search.result.more.other": "# till på denna sidan", + "search.result.term.missing": "Saknas", + "search.title": "Sök", + "select.language.title": "Välj språk", + "select.version.title": "Välj version", + "skip.link.title": "Gå till innehållet", + "source.link.title": "Gå till datakatalog", + "source.file.date.updated": "Senaste uppdaterad", + "source.file.date.created": "Skapad", + "tabs.title": "Flikar", + "toc.title": "Innehållsförteckning", + "top.title": "Tillbaka till toppen" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/th.html b/src/partials/languages/th.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "คัดลอก", + "clipboard.copied": "คัดลอกแล้ว", + "edit.link.title": "แก้ไขหน้านี้", + "footer.previous": "ก่อนหน้า", + "footer.next": "ต่อไป", + "meta.comments": "ความคิดเห็น", + "meta.source": "แหล่งที่มา", + "search.config.lang": "th", + "search.placeholder": "ค้นหา", + "search.result.placeholder": "พิมพ์เพื่อเริ่มค้นหา", + "search.result.none": "ไม่พบเอกสารที่ตรงกัน", + "search.result.one": "พบเอกสารที่ตรงกัน", + "search.result.other": "พบ # เอกสารที่ตรงกัน", + "skip.link.title": "ข้ามไปที่เนื้อหา", + "source.link.title": "ไปที่ Repository", + "source.file.date.updated": "สร้าง", + "source.file.date.created": "สร้าง", + "toc.title": "สารบัญ" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/tl.html b/src/partials/languages/tl.html @@ -0,0 +1,57 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopyahin sa clipboard", + "clipboard.copied": "Nakopya mula sa clipboard", + "edit.link.title": "I-edit ang pahinang ito", + "footer.previous": "Nakaraan", + "footer.next": "Susunod", + "footer.title": "Lagdang Pangwakas", + "header.title": "Pamuhatan", + "meta.comments": "Mga Komento", + "meta.source": "Pinagmulan", + "nav.title": "Nabigasyon", + "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", + "search.title": "Hanapin", + "select.language.title": "Pumili ng lenguwahe", + "select.version.title": "Pumili ng bersyon", + "skip.link.title": "I-skip tungo sa nilalaman", + "source.link.title": "Pumunta sa repository", + "source.file.date.updated": "Huling update", + "source.file.date.created": "Nagawa", + "tabs.title": "Mga tala", + "toc.title": "Talaan ng nilalaman", + "top.title": "Bumalik sa taas" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/tr.html b/src/partials/languages/tr.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Kopyala", + "clipboard.copied": "Kopyalandı", + "edit.link.title": "Düzenle", + "footer.previous": "Önceki", + "footer.next": "Sonraki", + "meta.comments": "Yorumlar", + "meta.source": "Kaynak", + "search.config.lang": "tr", + "search.placeholder": "Ara", + "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", + "skip.link.title": "Ana içeriğe geç", + "source.link.title": "Depoya git", + "source.file.date.updated": "Son Güncelleme", + "source.file.date.created": "Oluşturuldu", + "toc.title": "İçindekiler" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/uk.html b/src/partials/languages/uk.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Скопіювати в буфер", + "clipboard.copied": "Скопійовано в буфер", + "edit.link.title": "Редагувати сторінку", + "footer.previous": "Назад", + "footer.next": "Вперед", + "meta.comments": "Коментарі", + "meta.source": "Вихідний код", + "search.config.lang": "ru", + "search.placeholder": "Пошук", + "search.result.placeholder": "Розпочніть писати для пошуку", + "search.result.none": "Збігів не знайдено", + "search.result.one": "Знайдено 1 збіг", + "search.result.other": "Знайдено # збігів", + "skip.link.title": "Перейти до змісту", + "source.link.title": "Перейти до репозиторію", + "source.file.date.updated": "Останнє оновлення", + "source.file.date.created": "Створено", + "toc.title": "Зміст" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/ur.html b/src/partials/languages/ur.html @@ -0,0 +1,59 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "کلِپ بورڈ میں نقل کریں", + "clipboard.copied": "کلِپ بورڈ میں نقل کر دیا گیا", + "edit.link.title": "اس صفحے میں ترمیم کریں", + "footer.previous": "پچھلا", + "footer.next": "اگلا", + "footer.title": "ذیلی تحریر", + "header.title": "سر تحریر", + "meta.comments": "تبصرے", + "meta.source": "ذریعہ", + "nav.title": "رہنمائی", + "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": "گمشدہ", + "search.title": "تلاش", + "select.language.title": "زبان کا انتخاب کریں", + "select.version.title": "ورژن کا انتخاب کریں", + "skip.link.title": "براہِ راست مواد پر جائیں", + "source.link.title": "ریپازٹری پر جائیں", + "source.file.date.updated": "آخری بار تجدید", + "source.file.date.created": "تخلیق", + "tabs.title": "ٹیبز", + "toc.title": "فہرست", + "top.title": "واپس اوپر جائیں" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/uz.html b/src/partials/languages/uz.html @@ -0,0 +1,58 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Buferga nusxalash", + "clipboard.copied": "Buferga nusxalandi", + "edit.link.title": "Ushbu sahifani tahrirlash", + "footer.previous": "Oldingi sahifa", + "footer.next": "Keyingi sahifa", + "footer.title": "Pastgi qism", + "header.title": "Sarlavha", + "meta.comments": "Izohlar", + "meta.source": "Manba", + "nav.title": "Navigatsiya", + "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", + "search.title": "Qidirish", + "select.language.title": "Tilni tanlang", + "select.version.title": "Versiyani tanlang", + "skip.link.title": "Tarkibga o'tish", + "source.link.title": "Repozitoriyga o'tish", + "source.file.date.updated": "Oxirgi yangilanish", + "source.file.date.created": "Yaratildi", + "tabs.title": "Yorliqlar", + "toc.title": "Mundarija", + "top.title": "Yuqoriga qaytish" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/vi.html b/src/partials/languages/vi.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "Sao chép vào bộ nhớ", + "clipboard.copied": "Sao chép xong", + "edit.link.title": "Chỉnh sửa", + "footer.previous": "Trước", + "footer.next": "Sau", + "meta.comments": "Bình luận", + "meta.source": "Mã nguồn", + "search.config.lang": "vi", + "search.placeholder": "Tìm kiếm", + "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", + "skip.link.title": "Vào thẳng nội dung", + "source.link.title": "Đến kho lưu trữ mã nguồn", + "source.file.date.updated": "Cập nhật cuối cùng", + "source.file.date.created": "Tạo", + "toc.title": "Mục lục" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/zh-Hant.html b/src/partials/languages/zh-Hant.html @@ -0,0 +1,47 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "clipboard.copy": "拷貝", + "clipboard.copied": "已拷貝", + "edit.link.title": "編輯此頁", + "footer.previous": "上一頁", + "footer.next": "下一頁", + "meta.comments": "評論", + "meta.source": "來源", + "search.config.lang": "ja", + "search.config.pipeline": "trimmer, stemmer", + "search.config.separator": "[\\s\\-,。]+", + "search.placeholder": "搜尋", + "search.result.initializer": "正在初始化搜尋引擎", + "search.result.placeholder": "鍵入以開始檢索", + "search.result.none": "沒有找到符合條件的結果", + "search.result.one": "找到 1 个符合條件的結果", + "search.result.other": "# 個符合條件的結果", + "skip.link.title": "跳轉至", + "source.link.title": "前往倉庫", + "source.file.date.updated": "最後更新", + "source.file.date.created": "建立日期", + "toc.title": "目錄" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/zh-TW.html b/src/partials/languages/zh-TW.html @@ -0,0 +1,53 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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-Hant", + "announce.dismiss": "不再顯示此訊息", + "clipboard.copy": "複製", + "clipboard.copied": "已複製", + "consent.accept": "同意", + "consent.manage": "管理設定", + "consent.reject": "拒絕", + "edit.link.title": "編輯此頁", + "footer.previous": "上一頁", + "footer.next": "下一頁", + "meta.comments": "留言", + "meta.source": "來源", + "search.config.lang": "ja", + "search.config.pipeline": "trimmer, stemmer", + "search.config.separator": "[\\s\\- 、。,.?;]+", + "search.placeholder": "搜尋", + "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": "此頁尚有 # 個符合的項目", + "skip.link.title": "跳轉到", + "source.link.title": "前往倉庫", + "source.file.date.updated": "最後更新", + "source.file.date.created": "建立日期", + "toc.title": "目錄" +}[key] }}{% endmacro %} diff --git a/src/partials/languages/zh.html b/src/partials/languages/zh.html @@ -0,0 +1,64 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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", + "announce.dismiss": "不再显示此消息", + "clipboard.copy": "复制", + "clipboard.copied": "已复制", + "consent.accept": "同意", + "consent.manage": "管理设定", + "consent.reject": "拒绝", + "edit.link.title": "编辑此页", + "footer.previous": "上一页", + "footer.next": "下一页", + "footer.title": "页脚", + "header.title": "页眉", + "meta.comments": "评论", + "meta.source": "来源", + "nav.title": "导航栏", + "search.config.lang": "ja", + "search.config.pipeline": "trimmer, 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": "缺少", + "search.title": "查找", + "select.language.title": "选择当前语言", + "select.version.title": "选择当前版本", + "skip.link.title": "跳转至", + "source.link.title": "前往仓库", + "source.file.date.updated": "最后更新", + "source.file.date.created": "创建日期", + "tabs.title": "标签", + "toc.title": "目录", + "top.title": "回到页面顶部" +}[key] }}{% endmacro %} diff --git a/src/partials/logo.html b/src/partials/logo.html @@ -0,0 +1,29 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/partials/nav-item.html b/src/partials/nav-item.html @@ -0,0 +1,170 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +--> + +<!-- Wrap everything with a macro to reduce file roundtrips (see #2213) --> +{% macro render(nav_item, path, level) %} + + <!-- Determine class according to state --> + {% set class = "md-nav__item" %} + {% if nav_item.active %} + {% set class = class ~ " md-nav__item--active" %} + {% endif %} + + <!-- Main navigation item with nested items --> + {% if nav_item.children %} + + <!-- Determine whether to render item as a section --> + {% if "navigation.sections" in features and level == 1 + ( + "navigation.tabs" in features + ) %} + {% set class = class ~ " md-nav__item--section" %} + {% endif %} + + <!-- Render item with nested items --> + <li class="{{ class }} md-nav__item--nested"> + + <!-- Active checkbox expands items contained within nested section --> + {% set checked = "checked" if nav_item.active %} + {% if "navigation.expand" in features and not checked %} + <input + class="md-nav__toggle md-toggle md-toggle--indeterminate" + data-md-toggle="{{ path }}" + type="checkbox" + id="{{ path }}" + checked + /> + {% else %} + <input + class="md-nav__toggle md-toggle" + data-md-toggle="{{ path }}" + type="checkbox" + id="{{ path }}" + {{ checked }} + /> + {% endif %} + + <!-- 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 %} + + <!-- Render toggle to expand nested items --> + {% if not indexes %} + <label class="md-nav__link" for="{{ path }}"> + {{ nav_item.title }} + <span class="md-nav__icon md-icon"></span> + </label> + + <!-- Render link to index page + toggle --> + {% else %} + {% set index = indexes | first %} + {% set class = "md-nav__link--active" if index == page %} + <div class="md-nav__link md-nav__link--index {{ class }}"> + <a href="{{ index.url | url }}">{{ nav_item.title }}</a> + + <!-- Only render toggle if there's at least one more page --> + {% if nav_item.children | length > 1 %} + <label for="{{ path }}"> + <span class="md-nav__icon md-icon"></span> + </label> + {% endif %} + </div> + {% endif %} + + <!-- Render nested navigation --> + <nav + class="md-nav" + aria-label="{{ nav_item.title }}" + data-md-level="{{ level }}" + > + <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> + + <!-- Render nested item list --> + {% 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> + </li> + + <!-- Currently active page --> + {% elif nav_item == page %} + <li class="{{ class }}"> + {% set toc = page.toc %} + + <!-- Active checkbox expands items contained within nested section --> + <input + class="md-nav__toggle md-toggle" + data-md-toggle="toc" + 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 %} + + <!-- Render table of contents, if not empty --> + {% if toc %} + <label class="md-nav__link md-nav__link--active" for="__toc"> + {{ nav_item.title }} + <span class="md-nav__icon md-icon"></span> + </label> + {% endif %} + <a + href="{{ nav_item.url | url }}" + class="md-nav__link md-nav__link--active" + > + {{ nav_item.title }} + </a> + + <!-- Show table of contents --> + {% if toc %} + {% include "partials/toc.html" %} + {% endif %} + </li> + + <!-- Main navigation item --> + {% else %} + <li class="{{ class }}"> + <a href="{{ nav_item.url | url }}" class="md-nav__link"> + {{ nav_item.title }} + </a> + </li> + {% endif %} +{% endmacro %} + +<!-- Render current and nested navigation items --> +{{ render(nav_item, path, level) }} diff --git a/src/partials/nav.html b/src/partials/nav.html @@ -0,0 +1,68 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 class according to configuration --> +{% 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 %} + +<!-- Main navigation --> +<nav + class="{{ class }}" + aria-label="{{ lang.t('nav.title') }}" + 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 %} + + <!-- Render item list --> + <ul class="md-nav__list" data-md-scrollfix> + {% for nav_item in nav %} + {% set path = "__nav_" ~ loop.index %} + {% set level = 1 %} + {% include "partials/nav-item.html" %} + {% endfor %} + </ul> +</nav> diff --git a/src/partials/palette.html b/src/partials/palette.html @@ -0,0 +1,66 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +--> + +<!-- Primary colors --> +{% macro primary(key) %}{{ { + "red": "#ef5552", + "pink": "#e92063", + "purple": "#ab47bd", + "deep-purple": "#7e56c2", + "indigo": "#4051b5", + "blue": "#2094f3", + "light-blue": "#02a6f2", + "cyan": "#00bdd6", + "teal": "#009485", + "green": "#4cae4f", + "light-green": "#8bc34b", + "lime": "#cbdc38", + "yellow": "#ffec3d", + "amber": "#ffc105", + "orange": "#ffa724", + "deep-orange": "#ff6e42", + "brown": "#795649", + "grey": "#757575", + "blue-grey": "#546d78", + "black": "#000000", + "white": "#ffffff" +}[key] }}{% endmacro %} + +<!-- Accent colors --> +{% macro accent(key) %}{{ { + "red": "#ff1a47", + "pink": "#f50056", + "purple": "#df41fb", + "deep-purple": "#7c4dff", + "indigo": "#526cfe", + "blue": "#4287ff", + "light-blue": "#0091eb", + "cyan": "#00bad6", + "teal": "#00bda4", + "green": "#00c753", + "light-green": "#63de17", + "lime": "#b0eb00", + "yellow": "#ffd500", + "amber": "#ffaa00", + "orange": "#ff9100", + "deep-orange": "#ff6e42" +}[key] }}{% endmacro %} diff --git a/src/partials/search.html b/src/partials/search.html @@ -0,0 +1,103 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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"> + {% include ".icons/material/magnify.svg" %} + {% include ".icons/material/arrow-left.svg" %} + </label> + + <!-- Search options --> + <nav + class="md-search__options" + aria-label="{{ lang.t('search.title') }}" + > + + <!-- Button to share search --> + {% if "search.share" in features %} + <a + href="javascript:void(0)" + class="md-search__icon md-icon" + aria-label="{{ lang.t('search.share') }}" + data-clipboard + data-clipboard-text="" + data-md-component="search-share" + tabindex="-1" + > + {% include ".icons/material/share-variant.svg" %} + </a> + {% endif %} + + <!-- Button to reset search --> + <button + type="reset" + class="md-search__icon md-icon" + aria-label="{{ lang.t('search.reset') }}" + tabindex="-1" + > + {% include ".icons/material/close.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"></ol> + </div> + </div> + </div> + </div> +</div> diff --git a/src/partials/social.html b/src/partials/social.html @@ -0,0 +1,40 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 %} + {% 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="noopener" + title="{{ title | e }}" + class="md-social__link" + > + {% include ".icons/" ~ social.icon ~ ".svg" %} + </a> + {% endfor %} +</div> diff --git a/src/partials/source-file.html b/src/partials/source-file.html @@ -0,0 +1,44 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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/partials/source.html b/src/partials/source.html @@ -0,0 +1,37 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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.link.title') }}" + 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/partials/tabs-item.html b/src/partials/tabs-item.html @@ -0,0 +1,56 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 class according to state --> +{% if not class %} + {% set class = "md-tabs__link" %} + {% if nav_item.active %} + {% set class = class ~ " md-tabs__link--active" %} + {% endif %} +{% endif %} + +<!-- Main navigation item with nested items --> +{% if nav_item.children %} + {% set title = title | d(nav_item.title) %} + {% set nav_item = nav_item.children | first %} + + <!-- Recurse, if the first item has further nested items --> + {% if nav_item.children %} + {% include "partials/tabs-item.html" %} + + <!-- Render item --> + {% else %} + <li class="md-tabs__item"> + <a href="{{ nav_item.url | url }}" class="{{ class }}"> + {{ title }} + </a> + </li> + {% endif %} + +<!-- Main navigation item --> +{% else %} + <li class="md-tabs__item"> + <a href="{{ nav_item.url | url }}" class="{{ class }}"> + {{ nav_item.title }} + </a> + </li> +{% endif %} diff --git a/src/partials/tabs.html b/src/partials/tabs.html @@ -0,0 +1,39 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +--> + +<!-- Hack: unset variable, as we're using it recursively in tabs-item.html --> +{% set class = "" %} + +<!-- Navigation tabs --> +<nav + class="md-tabs" + aria-label="{{ lang.t('tabs.title') }}" + data-md-component="tabs" +> + <div class="md-tabs__inner md-grid"> + <ul class="md-tabs__list"> + {% for nav_item in nav %} + {% include "partials/tabs-item.html" %} + {% endfor %} + </ul> + </div> +</nav> diff --git a/src/partials/tags.html b/src/partials/tags.html @@ -0,0 +1,41 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 --> +{% if tags %} + <nav class="md-tags" {{ hidden }}> + {% for tag in tags %} + {% if tag.url %} + <a href="{{ tag.url | url }}" class="md-tag"> + {{ tag.name }} + </a> + {% else %} + <span class="md-tag">{{ tag.name }}</span> + {% endif %} + {% endfor %} + </nav> +{% endif %} diff --git a/src/partials/toc-item.html b/src/partials/toc-item.html @@ -0,0 +1,39 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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 }}"> + <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/partials/toc.html b/src/partials/toc.html @@ -0,0 +1,56 @@ +<!-- + Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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.title") %} +{% 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 %} + + <!-- + Hack: check whether the content contains a h1 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/plugins/__init__.py b/src/plugins/__init__.py diff --git a/src/plugins/search/__init__.py b/src/plugins/search/__init__.py diff --git a/src/plugins/search/plugin.py b/src/plugins/search/plugin.py @@ -0,0 +1,55 @@ +# Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com> + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION 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.contrib.search import SearchPlugin as BasePlugin +from mkdocs.contrib.search.search_index import SearchIndex as BaseIndex + +# ----------------------------------------------------------------------------- +# Class +# ----------------------------------------------------------------------------- + +# Search plugin with custom search index +class SearchPlugin(BasePlugin): + + # Override to use a custom search index + def on_pre_build(self, config): + super().on_pre_build(config) + self.search_index = SearchIndex(**self.config) + +# ----------------------------------------------------------------------------- + +# Search index with support for additional fields +class SearchIndex(BaseIndex): + + # Override to add additional fields for each page + def add_entry_from_context(self, page): + index = len(self._entries) + super().add_entry_from_context(page) + entry = self._entries[index] + + # Add document tags + if page.meta.get("tags"): + entry["tags"] = page.meta["tags"] + + # Add document boost for search + if "search" in page.meta: + search = page.meta["search"] + if "boost" in search: + entry["boost"] = search["boost"] diff --git a/src/plugins/tags/__init__.py b/src/plugins/tags/__init__.py diff --git a/src/plugins/tags/plugin.py b/src/plugins/tags/plugin.py @@ -0,0 +1,138 @@ +# Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com> + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION 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 os +import sys + +from collections import defaultdict +from markdown.extensions.toc import slugify +from mkdocs import utils +from mkdocs.commands.build import DuplicateFilter +from mkdocs.config.config_options import Type +from mkdocs.plugins import BasePlugin + +# ----------------------------------------------------------------------------- +# Class +# ----------------------------------------------------------------------------- + +# Tags plugin +class TagsPlugin(BasePlugin): + + # Configuration scheme + config_scheme = ( + ("tags_file", Type(str, required = False)), + ) + + # Initialize plugin + def __init__(self): + self.tags = defaultdict(list) + self.tags_file = None + self.slugify = None + + # Retrieve configuration for anchor generation + def on_config(self, config): + if "toc" in config["markdown_extensions"]: + 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 + def on_nav(self, nav, files, **kwargs): + file = self.config.get("tags_file") + if file: + self.tags_file = files.get_file_from_path(file) + if not self.tags_file: + log.error(f"Configuration error: {file} doesn't exist.") + sys.exit() + + # Add tags file to files + files.append(self.tags_file) + + # Build and render tags index page + def on_page_markdown(self, markdown, page, **kwargs): + 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, **kwargs): + if "tags" in page.meta: + context["tags"] = [ + self.__render_tag(tag) + for tag in page.meta["tags"] + ] + + # ------------------------------------------------------------------------- + + # 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): + content = [f"## <span class=\"md-tag\">{tag}</span>", ""] + for page in pages: + url = utils.get_relative_url( + page.file.src_path.replace(os.path.sep, "/"), + self.tags_file.src_path.replace(os.path.sep, "/") + ) + + # Ensure forward slashes, as we have to use the path of the source + # file which contains the operating system's path separator. + content.append("- [{}]({})".format( + page.meta.get("title", page.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): + if not self.tags_file or not self.slugify: + return dict(name = tag) + else: + url = self.tags_file.url + url += f"#{self.slugify(tag)}" + return dict(name = tag, url = url) + +# ----------------------------------------------------------------------------- +# Data +# ----------------------------------------------------------------------------- + +# Set up logging +log = logging.getLogger("mkdocs") +log.addFilter(DuplicateFilter())